diff --git a/README.md b/README.md index 47689661..3add7f39 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Work in Progress +## Tulane Dev Integration Branch + Sawt is a tool designed to bridge the communication gap between New Orleanians and their city council representatives. ## Prerequisites diff --git a/packages/googlecloud/functions/getanswer/inquirer.py b/packages/googlecloud/functions/getanswer/inquirer.py index 9bb1b3c2..ca005b98 100644 --- a/packages/googlecloud/functions/getanswer/inquirer.py +++ b/packages/googlecloud/functions/getanswer/inquirer.py @@ -171,7 +171,9 @@ def get_indepth_response_from_query(df, db, query, k): query = transform_query_for_date(query) doc_list = db.similarity_search_with_score(query, k=k) + docs = sort_retrived_documents(doc_list) + docs_page_content = append_metadata_to_content(doc_list) template = """ @@ -245,3 +247,4 @@ def answer_query( final_response = route_question(df, db_general, db_in_depth, query, response_type) return final_response + diff --git a/packages/googlecloud/functions/getanswer/main.py b/packages/googlecloud/functions/getanswer/main.py index 07717e30..ba0ffb10 100644 --- a/packages/googlecloud/functions/getanswer/main.py +++ b/packages/googlecloud/functions/getanswer/main.py @@ -5,7 +5,7 @@ import google.cloud.logging import functions_framework from supabase import create_client - +from dotenv import find_dotenv, load_dotenv from helper import parse_field, get_dbs from inquirer import answer_query import os @@ -19,6 +19,9 @@ db_general, db_in_depth, voting_roll_df = get_dbs() # Setup Supabase client +load_dotenv(find_dotenv()) + + try: supabase_url = os.environ["SUPABASE_URL_PRODUCTION"] supabase_key = os.environ["SUPABASE_SERVICE_KEY_PRODUCTION"] @@ -115,8 +118,12 @@ def getanswer(request): end = time.time() elapsed = int((end - start) * 1000) + update_supabase(responses_data, citations_data, card_id, elapsed) logging.info(f"Completed getanswer in {elapsed} seconds") print(f"\n\t--------- Completed getanswer in {elapsed} seconds --------\n") return ("Answer successfully submitted to Supabase", 200, headers) + + + diff --git a/packages/googlecloud/functions/getanswer/process_public_queries.py b/packages/googlecloud/functions/getanswer/process_public_queries.py new file mode 100644 index 00000000..32f080fc --- /dev/null +++ b/packages/googlecloud/functions/getanswer/process_public_queries.py @@ -0,0 +1,50 @@ +import pandas as pd +import numpy as np +import requests +import csv +import json +from tqdm import tqdm + +# Input CSV file with 'title' column +input_csv = "/Users/haydenoutlaw/Desktop/card_rows_export_2023-11-29.csv" +output_csv = "/Users/haydenoutlaw/Desktop/gpt4-varied-11-29.csv" + +# point to getanswer server +api_endpoint = "http://localhost:8080" + +# list of k values +k_list = [5, 10, 15] + +# get response from local getanswer server, store answers +def make_api_call(title, k_inp): + payload = {"query": title, "response_type": "in_depth", "card_id": 1, "k": k_inp} + response = requests.post(f"{api_endpoint}", json=payload) + rdict = json.loads(response.text) + card_type_out = rdict["card_type"] + citations_out = rdict["citations"] + responses_out = rdict["responses"] + return card_type_out, citations_out, responses_out, k_inp + +# Open CSV file in append mode +with open(output_csv, 'a', newline='', encoding='utf-8') as csv_file: + # define csv out file + csv_writer = csv.writer(csv_file) + csv_writer.writerow(["query", "response_id", "card_type", "citations", "responses", "k"]) + + # read inputs + df = pd.read_csv(input_csv) + + + print("Connected to getanswer at", api_endpoint) + print("K Values", k_list) + print("Generating Responses....") + + + # for all queries, get answers and write out one at a time + tqiter = enumerate(tqdm(df["title"])) + for i, query in tqiter: + for k_val in k_list: + card_type, citations, responses, k = make_api_call(query, k_val) + csv_writer.writerow([query, i, card_type, citations, responses, k]) + +print(f"Results saved to '{output_csv}'.") \ No newline at end of file diff --git a/packages/transcription/.gitignore b/packages/transcription/.gitignore new file mode 100644 index 00000000..333e1469 --- /dev/null +++ b/packages/transcription/.gitignore @@ -0,0 +1,8 @@ +.env +.log +__pycache__/ +transcripts-data/ +audio/ +cred/ +.vscode/ + diff --git a/packages/transcription/transcribe/README.md b/packages/transcription/transcribe/README.md new file mode 100644 index 00000000..b91b8e41 --- /dev/null +++ b/packages/transcription/transcribe/README.md @@ -0,0 +1,19 @@ +## TU Capstone- Transcription + +A generic API for fetching YouTube Audio and Transcripts. + +#### Required Credentials + - YOUTUBE_API_KEY + - GOOGLE_APPLICATION_CREDENTIALS +Create a cred folder containing cred.env variables according to dotenv configuration. + +### transcripts.py +Retrieves & downloads the x-most recent video transcripts from a YouTube Channel. + +### monitor.py +Retrieves & downloads the x-most recent video audio mp4s from a YouTube Channel. Future implemention should consider using Windows Task Scheduler to periodically monitor channel for new videos. + +#### Oauth.py +Helper authentication function. + + diff --git a/packages/transcription/transcribe/monitor.py b/packages/transcription/transcribe/monitor.py new file mode 100644 index 00000000..f9863326 --- /dev/null +++ b/packages/transcription/transcribe/monitor.py @@ -0,0 +1,71 @@ +from googleapiclient.discovery import build +#import youtube_dl Has BEEN DEPRECATED BY GERMAN GOVERNMENT +import os +from dotenv import load_dotenv +from pytube import YouTube +import oauth +# Initialize the YouTube Data API client + +env_vars = oauth.import_env_vars() +YOUTUBE_API_KEY = env_vars.get('YOUTUBE_API_KEY') +youtube = build('youtube', 'v3', developerKey=YOUTUBE_API_KEY) + +# Specify the YouTube channel ID +channel_id = 'UC8oPEsQe9a0v6TdJ4K_QXoA' # New Orleans City Council + +def get_latest_videos(channel_id, max_results=5): + """ + Fetches the latest x-number of videos from a YouTube channel. + + Args: + channel_id (str): The ID of the YouTube channel to monitor. + max_results (int): The maximum number of latest videos to fetch. Default is 5. + + Returns: + list: A list of video IDs for the latest videos. + """ + # Fetch channel details to get the ID of the uploads playlist + request = youtube.channels().list( + part='contentDetails', + id=channel_id + ) + response = request.execute() + + if not response.get('items'): + raise ValueError(f"No channel found with ID {channel_id}") + + playlist_id = response['items'][0]['contentDetails']['relatedPlaylists']['uploads'] + + request = youtube.playlistItems().list( + part='snippet', + playlistId=playlist_id, + maxResults=max_results + ) + response = request.execute() + + video_ids = [item['snippet']['resourceId']['videoId'] for item in response['items']] + + return video_ids + +def download_audio(video_ids): + """ + Downloads the audio of a list of YouTube videos using pytube. + + Args: + video_ids (list): A list of YouTube video IDs to download the audio for. + + Downloads: mp4 audio files of the desired Youtube videos. + """ + for video_id in video_ids: + yt = YouTube(f'https://www.youtube.com/watch?v={video_id}') + ys = yt.streams.filter(only_audio=True).first() + + # Download the audio stream to the specified output path + print(f'Downloading audio for {video_id}...') + ys.download(output_path=r'transcripts-data\audio', filename=video_id+".mp4") + +# Get the latest videos +video_ids = get_latest_videos(channel_id, 10) + +# Download the audio of the new videos +download_audio(video_ids) \ No newline at end of file diff --git a/packages/transcription/transcribe/oauth.py b/packages/transcription/transcribe/oauth.py new file mode 100644 index 00000000..07082eac --- /dev/null +++ b/packages/transcription/transcribe/oauth.py @@ -0,0 +1,21 @@ + +import os +from dotenv import load_dotenv + +def import_env_vars(): + os.chdir(r"packages\transcription") + load_dotenv(r"cred\cred.env") + + # Get credentials from environment variables + YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY") + CLIENT_ID = os.getenv("CLIENT_ID") + CLIENT_SECRET = os.getenv("CLIENT_SECRET") + GOOGLE_APPLICATION_CREDENTIALS= os.getenv("GOOGLE_APPLICATION_CREDENTIALS") + + os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = GOOGLE_APPLICATION_CREDENTIALS + + return { "YOUTUBE_API_KEY": YOUTUBE_API_KEY, + "CLIENT_ID": CLIENT_ID, + "CLIENT_SECRET": CLIENT_SECRET, + "GOOGLE_APPLICATION_CREDENTIALS": GOOGLE_APPLICATION_CREDENTIALS + } \ No newline at end of file diff --git a/packages/transcription/transcribe/transcripts.py b/packages/transcription/transcribe/transcripts.py new file mode 100644 index 00000000..4c256121 --- /dev/null +++ b/packages/transcription/transcribe/transcripts.py @@ -0,0 +1,67 @@ +from youtube_transcript_api import YouTubeTranscriptApi +from googleapiclient.discovery import build +import oauth +import json +import os + +# Get credentials from environment variables +env_vars = oauth.import_env_vars() +YOUTUBE_API_KEY = env_vars.get("YOUTUBE_API_KEY") +CLIENT_ID = env_vars.get("CLIENT_ID") +CLIENT_SECRET = env_vars.get("CLIENT_SECRET") +GOOGLE_APPLICATION_CREDENTIALS= env_vars.get("GOOGLE_APPLICATION_CREDENTIALS") + +def get_latest_videos(channel_id, max_results=5): + + """ + Fetches the latest x-number of videos from a YouTube channel. + + Args: + channel_id (str): The ID of the YouTube channel to monitor. + max_results (int): The maximum number of latest videos to fetch. Default is 5. + + Returns: + list: A list of video IDs for the latest videos. + """ + youtube = build('youtube', 'v3', developerKey=YOUTUBE_API_KEY) + + # Fetch channel details to get the ID of the uploads playlist + request = youtube.channels().list( + part='contentDetails', + id=channel_id + ) + response = request.execute() + + if not response.get('items'): + raise ValueError(f"No channel found with ID {channel_id}") + + playlist_id = response['items'][0]['contentDetails']['relatedPlaylists']['uploads'] + + request = youtube.playlistItems().list( + part='snippet', + playlistId=playlist_id, + maxResults=max_results + ) + response = request.execute() + + video_ids = [item['snippet']['resourceId']['videoId'] for item in response['items']] + + return video_ids + +def download_transcripts(video_ids): + for video_id in video_ids: + try: + # Grabs transcript for the video + transcript = YouTubeTranscriptApi.get_transcript(video_id) + print(transcript) + with open(f'transcripts-data\\YT_transcripts\\{video_id}_transcript.json', 'w+', encoding='utf-8') as file: + json.dump(transcript, file) + + print(f'Transcript for {video_id} saved successfully.') + + except Exception as e: + print(f'An error occurred while fetching the transcript for {video_id}: {e}') + +channel_id = "UC8oPEsQe9a0v6TdJ4K_QXoA" +video_ids = get_latest_videos(channel_id, 10) +download_transcripts(video_ids) \ No newline at end of file diff --git a/packages/transcription/whisper-model/README.md b/packages/transcription/whisper-model/README.md new file mode 100644 index 00000000..92dcc82e --- /dev/null +++ b/packages/transcription/whisper-model/README.md @@ -0,0 +1,28 @@ +# HF Whisper Transcript App +Application of [OpenAI Whisper-V2](https://huggingface.co/openai/whisper-large-v2) for audio file transcription. + + +## To Run +Configure [README.md]('README.md') +```yml +model: + #model size + #tiny, base, small, medium, large, large_v2 + size: "tiny" + # device for pytorch processing + device: "cpu" + # chunk length for audio processing + chunk_length: "10" + # batch size + batch_size: 1 +audio: + # path to audio file to process + path: "audio/trial_meeting.mp3" +transcript: + # location to save transcript + save_loc: "transcripts/trial_meeting_transcript.txt" +``` +Execute from CL: +```bash +python transcribe.py transcribe_config.yml +``` \ No newline at end of file diff --git a/packages/transcription/whisper-model/long_transcription.ipynb b/packages/transcription/whisper-model/long_transcription.ipynb new file mode 100644 index 00000000..65595181 --- /dev/null +++ b/packages/transcription/whisper-model/long_transcription.ipynb @@ -0,0 +1,247 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "582718ec5efa4efa9c8e3e2f6cbc1dfc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading (…)lve/main/config.json: 0%| | 0.00/1.94k [00:00(randint(0,177)); + const [userName, setUserName] = useState(""); + const [answered, setAnswered] = useState>(new Set()); + const [fullData, setFullData] = useState> | null>(null); + const [cards, setCards] = useState([]); + + const [cardArray, setCardArray] = useState> | null>(null); + + // Not the best way to do it-- we really should make each of these a new page and the next/prev buttons + // should be linked to the next/prev page. But this is a quick fix for now. + // const question_idArray = Array.from({ length: 291 }, (_, index) => index); + + // const handlePrevClick = () => { + // if (fullData) { + // setCardArray(fullData); + // //wraps around + // setCurrentIndex( + // (currentIndex - 1 + question_idArray.length) % question_idArray.length + // ); + // } else { + // alert("Please wait for the rest of the cards to finish loading..."); + // } + // }; + + const randQuestionId = () => { + return Math.floor(Math.random() * 291); + }; + + const shuffleArray = (array: Number[]) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + }; + + // const shuffledQuestionIds = shuffleArray(question_idArray); + const [currentIndex, setCurrentIndex] = useState(0); + // console.log(`index ${currentIndex}, val ${shuffledQuestionIds[0]}`); + const handleNextClick = () => { + // if (fullData) { + setCardArray(fullData); + //wraps around + setCurrentIndex(currentIndex + 1); + setAnswered(new Set()); + // } else { + // alert("Please wait for the rest of the cards to finish loading..."); + // } + }; + + //const handleNameChange = (e) => { + const handleNameChange = (e: React.ChangeEvent) => { + setUserName(e.target.value); + }; + + useEffect(() => { + const getCard = async () => { + const randId = randQuestionId(); + console.log("Fetching cards " + randId); + try { + const cardsArray: Array> = []; + const { data: newCards, error } = await supabase + .from(TABLES.FEEDBACK_CARDS) + .select("*") + .eq("question_id", randId); + if (newCards) { + setCards(newCards); + } + setCardArray(cardsArray); + console.log(cards); + } catch (error) { + console.error("Error fetching cards: ", error); + // Handle the error appropriately in your UI + } + // getCards(); + }; + getCard(); + }, [currentIndex]); // Run this effect only once when the component mounts + + // const getCards = async () => { + // const cardsArray: Array> = []; + // try { + // for (let i = 1; i < question_idArray.length; i++) { + // const { data: cards, error } = await supabase + // .from(TABLES.FEEDBACK_CARDS) + // .select("*") + // .eq("question_id", shuffledQuestionIds[i]); + + // if (error) { + // console.error("Error fetching cards: ", error); + // // Handle the error appropriately in your UI + // } + // // console.log(cards); + + // if (cards) { + // cardsArray.push(cards); + // } + // } + // setFullData(cardsArray); + // // console.log(fullData); + // //setCurrentIndex(Math.floor(Math.random() * cardsArray.length)); + // } catch (error) { + // console.error("Error fetching cards: ", error); + // // Handle the error appropriately in your UI + // } + // }; + + if (!cardArray) { + return
Loading...
; + } + + return ( +
+
+
+
+ +
+ +
+
+ + {answered.size === 3 && ( + + )} +
+
+
+ ); +} diff --git a/packages/web/components/BetaCard.tsx b/packages/web/components/BetaCard.tsx index 881e91ff..e4257c98 100644 --- a/packages/web/components/BetaCard.tsx +++ b/packages/web/components/BetaCard.tsx @@ -77,6 +77,41 @@ const BetaCard = ({ card }: { card: ICard }) => { }; }, [card.id]); + const handleCommentSubmit = async () => { + const newComment = { + card_id: card.id, + content: commentContent, + display_name: displayName, + created_at: new Date(), + }; + + + setComments((prevComments) => + prevComments + ? prevComments.filter((comment) => comment !== newComment) + : null + ); + + setDisplayName(""); // Resetting display name + setCommentContent(""); // Resetting comment content + + try { + const { data, error } = await supabase + .from("comments") + .insert([newComment]); + if (error) throw error; + setDisplayName(""); // Resetting display name after successful post + setCommentContent(""); // Resetting comment content after successful post + } catch (error) { + // If there's an error, revert the change to the comments + setComments((prevComments) => + prevComments + ? prevComments.filter((comment) => comment !== newComment) + : null + ); + } + }; + return (
{/* Card Header */} diff --git a/packages/web/components/Citation.tsx b/packages/web/components/Citation.tsx index 23e47aef..64958a91 100644 --- a/packages/web/components/Citation.tsx +++ b/packages/web/components/Citation.tsx @@ -9,6 +9,7 @@ import "./Citation.css"; interface CitationProps { citation: any; index: number; + fullscreen?: boolean; } const citationKeyMap: { [key: string]: string } = { @@ -18,7 +19,11 @@ const citationKeyMap: { [key: string]: string } = { source_url: "Source URL", }; -const Citation = ({ citation: originalCitation, index }: CitationProps) => { +const Citation = ({ + citation: originalCitation, + index, + fullscreen = false, +}: CitationProps) => { const hasMetadata = Object.values(originalCitation).some( (value) => value !== null && value !== "" ); @@ -35,7 +40,11 @@ const Citation = ({ citation: originalCitation, index }: CitationProps) => { const isYoutube = isYouTubeURL(source_url) && getYouTubeThumbnail(source_url); return ( -
+

#{index + 1}: {title} diff --git a/packages/web/components/CommentBoxes.tsx b/packages/web/components/CommentBoxes.tsx new file mode 100644 index 00000000..35d15d34 --- /dev/null +++ b/packages/web/components/CommentBoxes.tsx @@ -0,0 +1,63 @@ +import { ICard } from "@/lib/api"; +import { faComment } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from "react"; + +interface CommentBoxProps { + scores: Record; + card: ICard; + onSubmit: (data: { + comment: string; + card: ICard; + scores: Record; + index: number; + }) => void; + onReset: () => void; + index: number; +} + +export default function CommentBox({ + onSubmit, + card, + scores, + onReset, + index, +}: CommentBoxProps) { + const [comment, setComment] = useState(""); + // const [scores, setRubricScores] = useState>({}); + + const handleSubmit = () => { + onSubmit({ comment, card, scores, index }); + setComment(""); + onReset(); // Reset the scores after submission + }; + + return ( +

+
+ + +
+
+ +
+
+ ); +} diff --git a/packages/web/components/Navbar.tsx b/packages/web/components/Navbar.tsx index 2ebaf410..0f33a1d7 100644 --- a/packages/web/components/Navbar.tsx +++ b/packages/web/components/Navbar.tsx @@ -15,6 +15,10 @@ export const navLinks = [ id: "tips", title: "How to use", }, + { + id: "feedback", + title: "Feedback", + }, ]; const Navbar = () => { diff --git a/packages/web/components/Rubric.tsx b/packages/web/components/Rubric.tsx new file mode 100644 index 00000000..3e6dfadf --- /dev/null +++ b/packages/web/components/Rubric.tsx @@ -0,0 +1,73 @@ + + +interface RubricProps { + criteria: Array<{ + id: string; + description: string; + }>; + scores: Record; + onScoreChange: (criterionId: string, score: number) => void; +} + + +const Rubric: React.FC = ({ criteria, scores, onScoreChange }) => { // This state will hold the scores for each criterion + + + + const circleButtonStyle = (score: number, criterionId: string) => ({ + width: '40px', // Circle diameter + height: '40px', // Circle diameter + borderRadius: '50%', // Make it round + margin: '5px', + fontWeight: scores[criterionId] === score ? 'bold' : 'normal', + outline: 'none', + border: scores[criterionId] === score ? '2px solid blue' : '1px solid grey' + }); + const submitButtonStyle = { + padding: '10px 20px', + fontSize: '16px', + color: 'white', + backgroundColor: '#007bff', + border: 'none', + borderRadius: '5px', + cursor: 'pointer', + outline: 'none', + marginTop: '20px', + }; + + const containerStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + padding: '20px', + }; + + return ( +
+
+ {criteria.map(criterion => ( +
+ +
+ {[1, 2, 3, 4, 5].map(score => ( + + ))} +
+
+ ))} + {/* */} + {/* */} + +
+
+ ); + }; + + export default Rubric; \ No newline at end of file diff --git a/packages/web/components/ThreeCardLayout.css b/packages/web/components/ThreeCardLayout.css new file mode 100644 index 00000000..59ed0d0b --- /dev/null +++ b/packages/web/components/ThreeCardLayout.css @@ -0,0 +1,61 @@ +/* ThreeCardLayout.css */ +.dropdown-container { + position: relative; + } + + .dropdown-header { + cursor: pointer; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + display: flex; + justify-content: space-between; + align-items: center; + } + + .dropdown-content { + position: absolute; + top: 100%; + left: 0; + background-color: #fff; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + padding: 8px; + z-index: 1; + overflow: scroll; + } + + /* Rotate chevron down icon when dropdown is open */ + .rotate-180 { + transform: rotate(180deg); + } + + +.dropdown-content { + position: absolute; + top: 100%; + left: 0; + background-color: #fff; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + padding: 8px; + z-index: 1; + width: 100%; + } + + + +/* Adjust styles for screens smaller than 600px */ +@media (max-width: 600px) { + .dropdown-header { + font-size: 14px; + } + + .dropdown-content { + padding: 10px; + } + } + + \ No newline at end of file diff --git a/packages/web/components/ThreeCardLayout.tsx b/packages/web/components/ThreeCardLayout.tsx new file mode 100644 index 00000000..5fd40aec --- /dev/null +++ b/packages/web/components/ThreeCardLayout.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { ICard } from "@/lib/api"; +// import { CARD_SHOW_PATH, getPageURL } from "@/lib/paths"; +import { supabase } from "@/lib/supabase/supabaseClient"; +// import Link from "next/link"; +import Rubric from "@/components/Rubric"; +import { TABLES } from "@/lib/supabase/db"; +import { + faCheckCircle, + faChevronDown, + faCircleXmark, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from "react"; +import Citation from "./Citation"; +import CommentBox from "./CommentBoxes"; + +const criteria = [ + { id: "Accuracy", description: "Accuracy" }, + { id: "Helpfulness", description: "Helpfulness" }, + { id: "Balance", description: "Balance" }, + // Add more criteria as needed +]; + +const NUM_CARDS_PER_SET = 3; +const RESET_SCORE = 1; + +function getRandomExcluding(set: Set, n: number): number | null { + if (set.size >= n) { + return null; // No available number + } + + let randomNum; + do { + randomNum = Math.floor(Math.random() * n); + } while (set.has(randomNum)); + + return randomNum; +} + +export default function ThreeCardLayout({ + cards, + userName, + answered, + setAnswered, +}: { + cards: Array; + userName: string; + answered: Set; + setAnswered: (_: any) => void; +}) { + const [scores, setScores] = useState>({}); + const [activeTab, setActiveTab] = useState(0); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + // Function to update scores + const handleScoreChange = (criterionId: string, score: number) => { + setScores((prevScores) => ({ ...prevScores, [criterionId]: score })); + }; + + // Function to reset scores + const resetScores = () => { + // Reset logic - assuming each score should reset to 1 + // const resettedScores = Object.keys(scores).reduce((acc, criterionId) => { + // acc[criterionId] = 1; + // return acc; + // }, {}); + + const resetScores = { + Accuracy: RESET_SCORE, + Helpfulness: RESET_SCORE, + Balance: RESET_SCORE, + }; + + setScores(resetScores); + setActiveTab(0); + }; + + //Function that sends comments to supabase under respective card.comment + const submitCommentFeedback = async ({ + scores, + comment, + card, + index, + }: { + scores: Record; + comment: string; + card: ICard; + index: number; + }) => { + try { + const { data: existingCard, error: fetchError } = await supabase + .from(TABLES.FEEDBACK_CARDS) + .select("question_id, id") + .eq("id", card.id) + .single(); + + // order by random + // select all ids load them to an array + // then independent supabase fetch the card with that random id + // --on button click + + if (fetchError) { + throw fetchError; + } + const user_id = `${userName}_${Date.now()}`; + + const { data, error } = await supabase + .from(TABLES.USER_FEEDBACK) + .insert([ + { + question_id: existingCard.question_id, + response_id: existingCard.id, + user_id: user_id, + comment: comment, + accuracy: scores["Accuracy"], + helpfulness: scores["Helpfulness"], + balance: scores["Balance"], + }, + ]) + .select(); + + if (error) { + throw error; + } + + const newAnswered = new Set(answered); + newAnswered.add(index); + setAnswered(newAnswered); + + console.log("Submit feedback " + newAnswered.size); + if (newAnswered.size < NUM_CARDS_PER_SET) { + const randUnscored = getRandomExcluding(newAnswered, NUM_CARDS_PER_SET); + console.log("Unscored " + randUnscored); + if (!!randUnscored) setActiveTab(randUnscored); + } + } catch (error) {} + }; + + const Tabs = () => { + return Array.from({ length: NUM_CARDS_PER_SET }, (_, index) => { + const isAnswered = answered.has(index); + const isSelected = activeTab === index; + return ( +
{ + setActiveTab(index); + }} + > + + Option {index + 1}{" "} + + +
+ ); + }); + }; + + return ( +
+
+ +
+ {cards && + cards.map((card, i) => + i === activeTab ? ( +
+
+

{card.title}

+ + {/* */} + {/* LINK DOES the modal-- Need to change card.id to questionID to refer back to original card ID */} +
+
+ + {card.is_mine ? "You | " : null} + +
+ +
+ {card.responses && + card.responses.map((element, index) => ( +

+ {element.response} +

+ ))} + {card.citations && card.citations.length > 0 && ( +
+
setIsDropdownOpen(!isDropdownOpen)} + > + Citations + +
+ {isDropdownOpen && ( +
+ {card.citations.map((citation, index) => { + const { + URL: source_url, + Name: source_name, + Title: source_title, + Published: source_publish_date, + } = citation as any; + const adaptedCitation = { + source_name, + source_publish_date, + source_title, + source_url, + }; + return ( + + ); + })} +
+ )} +
+ )} +
+
+ + {!answered.has(i) && ( +
+
+ {/* */} + + + + +
+ )} +
+
+ ) : null + )} +
+ ); +} diff --git a/packages/web/lib/supabase/db.ts b/packages/web/lib/supabase/db.ts index 2ca2224b..a7cd4888 100644 --- a/packages/web/lib/supabase/db.ts +++ b/packages/web/lib/supabase/db.ts @@ -1,4 +1,6 @@ export const TABLES = { USER_QUERIES: "user_queries", CARDS: "cards", + FEEDBACK_CARDS: "feedback_cards", + USER_FEEDBACK: "user_feedback", }; diff --git a/packages/web/supabase/migrations/20231207000743_feedback_cards.sql b/packages/web/supabase/migrations/20231207000743_feedback_cards.sql new file mode 100644 index 00000000..7c7488f9 --- /dev/null +++ b/packages/web/supabase/migrations/20231207000743_feedback_cards.sql @@ -0,0 +1,97 @@ +-- feedback cards +create table "public"."feedback_cards" ( + "id" uuid not null default gen_random_uuid(), + "title" text, + "question_id" bigint, + "card_type" text, + "citations" jsonb, + "responses" jsonb, + "k" bigint +); + + + + + +-- user feedback + +create table "public"."user_feedback" ( + "created_at" timestamp with time zone not null default now(), + "user_id" text, + "comment" jsonb, + "question_id" bigint, + "response_id" uuid, + "accuracy" integer, + "helpfulness" integer, + "balance" integer +); + + + + +alter table "public"."user_feedback" add column "id" uuid not null default gen_random_uuid(); + +CREATE UNIQUE INDEX feedback_cards_id_key ON public.feedback_cards USING btree (id); + +CREATE UNIQUE INDEX feedback_cards_pkey ON public.feedback_cards USING btree (id); + +CREATE UNIQUE INDEX user_feedback_pkey ON public.user_feedback USING btree (id); + +alter table "public"."feedback_cards" add constraint "feedback_cards_pkey" PRIMARY KEY using index "feedback_cards_pkey"; + +alter table "public"."user_feedback" add constraint "user_feedback_pkey" PRIMARY KEY using index "user_feedback_pkey"; + +alter table "public"."feedback_cards" add constraint "feedback_cards_id_key" UNIQUE using index "feedback_cards_id_key"; + +alter table "public"."feedback_cards" enable row level security; + + +create policy "Enable insert access for all users" +on "public"."feedback_cards" +as permissive +for insert +to public +with check (true); + + +create policy "Enable read access for all users" +on "public"."feedback_cards" +as permissive +for select +to public +using (true); + + +create policy "Enable update access for all users" +on "public"."feedback_cards" +as permissive +for update +to public +using (true) +with check (true); + +alter table "public"."user_feedback" enable row level security; + +create policy "anyone can insert" +on "public"."user_feedback" +as permissive +for insert +to public +with check (true); + + +create policy "anyone can read" +on "public"."user_feedback" +as permissive +for select +to public +using (true); + + +create policy "anyone can update" +on "public"."user_feedback" +as permissive +for update +to public +using (true) +with check (true); \ No newline at end of file diff --git a/packages/whisper/README.md b/packages/whisper/README.md new file mode 100644 index 00000000..92dcc82e --- /dev/null +++ b/packages/whisper/README.md @@ -0,0 +1,28 @@ +# HF Whisper Transcript App +Application of [OpenAI Whisper-V2](https://huggingface.co/openai/whisper-large-v2) for audio file transcription. + + +## To Run +Configure [README.md]('README.md') +```yml +model: + #model size + #tiny, base, small, medium, large, large_v2 + size: "tiny" + # device for pytorch processing + device: "cpu" + # chunk length for audio processing + chunk_length: "10" + # batch size + batch_size: 1 +audio: + # path to audio file to process + path: "audio/trial_meeting.mp3" +transcript: + # location to save transcript + save_loc: "transcripts/trial_meeting_transcript.txt" +``` +Execute from CL: +```bash +python transcribe.py transcribe_config.yml +``` \ No newline at end of file diff --git a/packages/whisper/long_transcription.ipynb b/packages/whisper/long_transcription.ipynb new file mode 100644 index 00000000..65595181 --- /dev/null +++ b/packages/whisper/long_transcription.ipynb @@ -0,0 +1,247 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "582718ec5efa4efa9c8e3e2f6cbc1dfc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading (…)lve/main/config.json: 0%| | 0.00/1.94k [00:00