From 2cf56f7fded764b42d3317d6c79c74a890298727 Mon Sep 17 00:00:00 2001 From: Marvin Arnold Date: Wed, 6 Dec 2023 21:42:45 -0600 Subject: [PATCH] feat: migrations and feedback UI --- packages/web/README.md | 53 ++++- .../app/{userFeedback => feedback}/page.tsx | 119 +++++----- packages/web/components/CommentBoxes.tsx | 81 ++++--- packages/web/components/Navbar.tsx | 4 +- packages/web/components/ThreeCardLayout.tsx | 221 +++++++++++------- packages/web/lib/supabase/db.ts | 2 + .../20231207000743_feedback_cards.sql | 97 ++++++++ 7 files changed, 396 insertions(+), 181 deletions(-) rename packages/web/app/{userFeedback => feedback}/page.tsx (55%) create mode 100644 packages/web/supabase/migrations/20231207000743_feedback_cards.sql diff --git a/packages/web/README.md b/packages/web/README.md index a5ddc54e..eadc5a96 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -26,14 +26,55 @@ yarn dev pnpm dev ``` -### Supabase migrations +## Migration instructions -The first time you setup a new Supabase project, migrations must be applied. +To make changes to the schema moving forward, you can update the DB directly from Supabase DB. When ready, run `supabase db remote commit`. You will need to have Docker running locally for this to succeed. When complete, there will be a new file in `supabase/migrations` with a name like `20230821153353_remote_commit.sql`. Commit this file to source control and it will automatically be applied to the DB when merged. -``` -supabase login -supabase link --project-ref $PROJECT_ID +### New project + +```bash +# from nextjs project +cd packages/web + +# connect to project +supabase link --project-ref weqbsjuunfkxuyhsutzx + +# sync project with existing migrations supabase db push + +# pull remote project changes to local code +supabase db remote commit ``` -To make changes to the schema moving forward, you can update the DB directly from Supabase DB. When ready, run `supabase db remote commit`. You will need to have Docker running locally for this to succeed. When complete, there will be a new file in `supabase/migrations` with a name like `20230821153353_remote_commit.sql`. Commit this file to source control and it will automatically be applied to the DB when merged. +This will create a new file in `packages/web/supabase/migrations`. For some reason, Supabase adds a bunch of junk that you can remove from the generated migration. + +```sql +-- delete all this +alter table "auth"."saml_relay_states" add column "flow_state_id" uuid; + +alter table "auth"."sessions" add column "ip" inet; + +alter table "auth"."sessions" add column "refreshed_at" timestamp without time zone; + +alter table "auth"."sessions" add column "user_agent" text; + +CREATE INDEX flow_state_created_at_idx ON auth.flow_state USING btree (created_at DESC); + +CREATE INDEX mfa_challenge_created_at_idx ON auth.mfa_challenges USING btree (created_at DESC); + +CREATE INDEX mfa_factors_user_id_idx ON auth.mfa_factors USING btree (user_id); + +CREATE INDEX refresh_tokens_updated_at_idx ON auth.refresh_tokens USING btree (updated_at DESC); + +CREATE INDEX saml_relay_states_created_at_idx ON auth.saml_relay_states USING btree (created_at DESC); + +CREATE INDEX sessions_not_after_idx ON auth.sessions USING btree (not_after DESC); + +alter table "auth"."saml_relay_states" add constraint "saml_relay_states_flow_state_id_fkey" FOREIGN KEY (flow_state_id) REFERENCES auth.flow_state(id) ON DELETE CASCADE not valid; + +alter table "auth"."saml_relay_states" validate constraint "saml_relay_states_flow_state_id_fkey"; +``` + +## Importing large data + +Increase session timeout: `alter role authenticator set statement_timeout = '120s';` diff --git a/packages/web/app/userFeedback/page.tsx b/packages/web/app/feedback/page.tsx similarity index 55% rename from packages/web/app/userFeedback/page.tsx rename to packages/web/app/feedback/page.tsx index f6ac43c9..65c58cd5 100644 --- a/packages/web/app/userFeedback/page.tsx +++ b/packages/web/app/feedback/page.tsx @@ -1,12 +1,12 @@ - "use client"; // Import necessary modules and components -import { supabase } from '../../lib/supabase/supabaseClient'; -import ThreeCardLayout from '../../components/ThreeCardLayout'; +import ThreeCardLayout from "../../components/ThreeCardLayout"; +import { supabase } from "../../lib/supabase/supabaseClient"; // import NextButton from '@/components/NextButton'; -import { useState, useEffect } from "react"; -import { ICard } from '@/lib/api'; +import { ICard } from "@/lib/api"; +import { TABLES } from "@/lib/supabase/db"; +import { useEffect, useState } from "react"; export const dynamic = "force-dynamic"; @@ -14,74 +14,74 @@ export default function UserFeedback() { // const [currentIndex, setCurrentIndex] = useState(randint(0,177)); const [userName, setUserName] = useState(""); const [currentIndex, setCurrentIndex] = useState(0); + const [answered, setAnswered] = useState>(new Set()); const [fullData, setFullData] = useState> | null>(null); 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: 98 }, (_, 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 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 handleNextClick = () => { if (fullData) { setCardArray(fullData); //wraps around setCurrentIndex((currentIndex + 1) % question_idArray.length); + 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 () => { try { const cardsArray: Array> = []; const { data: cards, error } = await supabase - .from('sawt_cards') - .select('*') + .from(TABLES.FEEDBACK_CARDS) + .select("*") .eq("question_id", 0); if (cards) { cardsArray.push(cards); } setCardArray(cardsArray); console.log(cards); - }catch (error) { + } catch (error) { console.error("Error fetching cards: ", error); // Handle the error appropriately in your UI } getCards(); - } - getCard(); + }; + getCard(); }, []); // 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('sawt_cards') - .select('*') + .from(TABLES.FEEDBACK_CARDS) + .select("*") .eq("question_id", i); - + if (error) { console.error("Error fetching cards: ", error); // Handle the error appropriately in your UI @@ -91,49 +91,54 @@ export default function UserFeedback() { if (cards) { cardsArray.push(cards); } - } + } setFullData(cardsArray); - console.log(fullData); + // 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/CommentBoxes.tsx b/packages/web/components/CommentBoxes.tsx index 0b7cd545..f8602ef2 100644 --- a/packages/web/components/CommentBoxes.tsx +++ b/packages/web/components/CommentBoxes.tsx @@ -1,54 +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 + scores: Record; + card: ICard; + onSubmit: (data: { + comment: string; card: ICard; - onSubmit: (data: { comment: string, card: ICard, scores: Record }) => void; - onReset: () => void; + scores: Record; + index: number; + }) => void; + onReset: () => void; + index: number; } -export default function CommentBox({ onSubmit, card, scores, onReset }: CommentBoxProps) { - const [comment, setComment] = useState(""); - // const [scores, setRubricScores] = useState>({}); - - const handleSubmit = () => { - onSubmit({ comment, card, scores}); - setComment(""); - onReset(); // Reset the scores after submission - }; +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 ( - -
-
+ return ( +
+
-
-
- -
+
+
+ +
- ); -} \ No newline at end of file + ); +} diff --git a/packages/web/components/Navbar.tsx b/packages/web/components/Navbar.tsx index 3ae1e3a2..0f33a1d7 100644 --- a/packages/web/components/Navbar.tsx +++ b/packages/web/components/Navbar.tsx @@ -16,8 +16,8 @@ export const navLinks = [ title: "How to use", }, { - id: "userFeedback", - title: "User Feedback", + id: "feedback", + title: "Feedback", }, ]; diff --git a/packages/web/components/ThreeCardLayout.tsx b/packages/web/components/ThreeCardLayout.tsx index d4b07a00..471b9ed2 100644 --- a/packages/web/components/ThreeCardLayout.tsx +++ b/packages/web/components/ThreeCardLayout.tsx @@ -1,143 +1,204 @@ - "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, + faCircleXmark, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useState } from "react"; import CommentBox from "./CommentBoxes"; -import Rubric from '@/components/Rubric'; - const criteria = [ - { id: 'Accuracy', description: 'Accuracy' }, - { id: 'Helpfulness', description: 'Helpfulness' }, - { id: 'Balance', description: 'Balance' } + { id: "Accuracy", description: "Accuracy" }, + { id: "Helpfulness", description: "Helpfulness" }, + { id: "Balance", description: "Balance" }, // Add more criteria as needed ]; -export default function ThreeCardLayout({ cards, userName }: { cards: Array, userName: string }) { - +const NUM_CARDS_PER_SET = 3; + +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); // Function to update scores const handleScoreChange = (criterionId: string, score: number) => { - setScores(prevScores => ({ ...prevScores, [criterionId]: score })); + 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; + // 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": 1, - "Helpfulness": 1, - "Balance": 1 + Accuracy: 1, + Helpfulness: 1, + Balance: 1, }; - setScores(resetScores); + setScores(resetScores); }; //Function that sends comments to supabase under respective card.comment const submitCommentFeedback = async ({ scores, comment, - card + card, + index, }: { scores: Record; comment: string; card: ICard; + index: number; }) => { try { const { data: existingCard, error: fetchError } = await supabase - .from("sawt_cards") + .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 - - - + // --on button click + if (fetchError) { throw fetchError; } const user_id = `${userName}_${Date.now()}`; - - const { data, error } = await supabase - .from("UserFeedback") - .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() - + .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); } catch (error) {} }; + const Tabs = () => { + return Array.from({ length: NUM_CARDS_PER_SET }, (_, index) => { + const isAnswered = answered.has(index); + return ( +
{ + setActiveTab(index); + }} + > + Option {index + 1}{" "} + {isAnswered ? ( + + ) : ( + + )} +
+ ); + }); + }; + return (
- {cards && cards.map((card) => ( -
- -
-

{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} - -
- +
+ +
+ {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.responses && card.responses.map((element, index) => ( - -

- {element.response} - -

- ))} +
+ + {card.is_mine ? "You | " : null} + +
+ +
+ {card.responses && + card.responses.map((element, index) => ( +

+ {element.response} +

+ ))} +
-
-
- {/* */} - - - - {/* */} - - - - -
-
- )) - } - -
+ {!answered.has(i) && ( +
+
+ {/* */} + + + + +
+ )} +
+
+ ) : null + )} +
); -} \ No newline at end of file +} 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