Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/save-paraphraser-in-DB #97

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c9bc436
feat: create utils for mustating paraphraser
Ali-Hussein-dev Feb 24, 2024
5b34e04
feat: update type schema
Ali-Hussein-dev Feb 26, 2024
210e752
feat: create store with init props for paraphraser
Ali-Hussein-dev Feb 27, 2024
76f3df9
feat: type actions & query hooks
Ali-Hussein-dev Feb 27, 2024
8099909
feat: implement with Zustand
Ali-Hussein-dev Feb 28, 2024
3377dff
Merge remote-tracking branch 'origin/main' into feat/save-paraphraser…
Ali-Hussein-dev Feb 28, 2024
6a46219
feat: update default tones
Ali-Hussein-dev Feb 28, 2024
2a2c5f0
feat: manage form state with useForm
Ali-Hussein-dev Feb 28, 2024
5ceb433
Merge remote-tracking branch 'origin/main' into feat/save-paraphraser…
Ali-Hussein-dev Feb 28, 2024
0abe81c
style: tweak the temperature UI
Ali-Hussein-dev Mar 2, 2024
f30e66f
Merge remote-tracking branch 'origin/main' into feat/save-paraphraser…
Ali-Hussein-dev Mar 5, 2024
00fa81e
feat: use upsert instead & throw an error if it fails
Ali-Hussein-dev Mar 5, 2024
09046dd
refactor: avoid overriding onSuccess callback
Ali-Hussein-dev Mar 5, 2024
1afed03
fix: remove prop
Ali-Hussein-dev Mar 6, 2024
489f884
Merge remote-tracking branch 'origin/main' into feat/save-paraphraser…
Ali-Hussein-dev Mar 6, 2024
a0c972a
Merge remote-tracking branch 'origin/main' into feat/save-paraphraser…
Ali-Hussein-dev Mar 6, 2024
35bf881
security: add schema validation
Ali-Hussein-dev Mar 7, 2024
d024971
feat: update schema from DB
Ali-Hussein-dev Mar 7, 2024
0ff27dc
feat: pass data directly from DB
Ali-Hussein-dev Mar 7, 2024
907371e
refactor: rebuild the paraphraser section
Ali-Hussein-dev Mar 7, 2024
8dbc1a8
feat: Add Logout Btn & AvatarMenu
Ali-Hussein-dev Mar 7, 2024
aeaf032
fix: export as default in page file
Ali-Hussein-dev Mar 7, 2024
3dc2523
fix: fix type errors
Ali-Hussein-dev Mar 7, 2024
6666c3b
Merge remote-tracking branch 'origin/main' into feat/save-paraphraser…
Ali-Hussein-dev Mar 8, 2024
1a3683b
feat: create reusable hook for isAuth
Ali-Hussein-dev Mar 8, 2024
1c83cd1
feat: render avatar menu conditionally
Ali-Hussein-dev Mar 8, 2024
1fbad03
feat: mix changes
Ali-Hussein-dev Mar 8, 2024
862d0e4
refactor: render only login link
Ali-Hussein-dev Mar 8, 2024
88c7132
fix: fix type error
Ali-Hussein-dev Mar 8, 2024
0d4c486
fix: import client comp from separate file
Ali-Hussein-dev Mar 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion app/tools/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from "react";
import { SharedToolsLayout } from "@/components/shared-tools-layout";
import { LogoutButton } from "@/components/auth-buttons";
import { AvatarMenu } from "@/components/avatar-menu";

export const metadata = {
title: "Tools List",
Expand All @@ -11,5 +13,21 @@ export default function ToolsLayout({
}: {
children: React.ReactNode;
}) {
return <SharedToolsLayout>{children}</SharedToolsLayout>;
return (
<SharedToolsLayout
LoginLink={
<AvatarMenu
LogoutButton={
<LogoutButton
variant="subtle"
w="100%"
classNames={{ root: "flex-row-start" }}
/>
}
/>
}
>
{children}
</SharedToolsLayout>
);
}
214 changes: 3 additions & 211 deletions app/tools/paraphraser/page.tsx
Original file line number Diff line number Diff line change
@@ -1,213 +1,5 @@
"use client";
import {
Checkbox,
Menu,
ActionIcon,
Button,
Slider,
Text,
Divider,
} from "@mantine/core";
import { useChat } from "ai/react";
import { TbChevronDown } from "react-icons/tb";
import * as React from "react";
import { AiOutlineClear } from "react-icons/ai";
import { DynamicCustomTextarea } from "@/components/Mantine/custom-textarea";
import { CustomMenu } from "@/components/Mantine/custom-menu";
import { Markdown } from "@/components/shared/Markdown";
import { useTextOptimizer } from "@/hooks/use-text-optimizer";
import { CustomTones } from "@/components/custom-tones";
import { IoStopCircleOutline } from "react-icons/io5";
import { MdClear } from "react-icons/md";
import { CopyButton } from "@/components/shared/copy-button";
import { ToolContainer } from "@/components/tool-container";
import { ParaphraserMain } from "@/components/paraphraser";

function Temperature() {
const temperature = useTextOptimizer((s) => s.temperature);
const setTemperature = useTextOptimizer((s) => s.setTemperature);
return (
<div className="pb-4">
<Text size="sm" mb="4px">
Creativity Level ({temperature}/2)
</Text>
<Slider
value={temperature}
onChange={setTemperature}
min={0.1}
max={2}
step={0.1}
/>
</div>
);
}
//======================================
const CustomizationMenu = ({
selected,
setSelected,
}: {
selected: string[];
setSelected: (s: string[]) => void;
}) => {
const tones = useTextOptimizer((s) => s.tones);
return (
<CustomMenu width={"270"} position="bottom-start" closeOnItemClick={false}>
<Menu.Target>
<Button
radius="lg"
pl="0"
leftSection={<CustomTones />}
rightSection={<TbChevronDown />}
variant="light"
onClick={(e) => {
e.stopPropagation();
}}
>
Customize
</Button>
</Menu.Target>
<Menu.Dropdown p={8} mah="320px" className="!overflow-y-auto">
<Text>Select Output Tone</Text>
<Checkbox.Group
value={selected}
onChange={(value) => {
setSelected(value);
}}
>
{tones.map((item) => (
<Menu.Item key={item.label} py="xs" px="4px">
<Checkbox
key={item.label}
value={item.value}
label={item.label}
classNames={{
labelWrapper: "w-full",
}}
/>
</Menu.Item>
))}
</Checkbox.Group>
<Divider color="#475569" my="sm" />
<Temperature />
</Menu.Dropdown>
</CustomMenu>
);
};

export default function Paraphraser() {
const history = useTextOptimizer((s) => s.history);
const setHistory = useTextOptimizer((s) => s.setHistory);
const temperature = useTextOptimizer((s) => s.temperature);
const [selected, setSelected] = React.useState<string[]>([]);
const {
messages,
setMessages,
input,
setInput,
handleInputChange,
handleSubmit,
isLoading,
} = useChat({
api: "/api/text-optimizer",
initialMessages: history,
onResponse: () => {
setInput(input);
},
onFinish: (d) => {
setHistory([...history.slice(-5), d]);
},
body: {
tones: selected.join(", "),
temperature,
},
});
return (
<ToolContainer
title="paraphraser"
showRating={messages.length > 0 && (!isLoading || messages.length > 1)}
>
{/* //---------------------------------------------------INPUT AREA */}
<form className="space-y-4" onSubmit={handleSubmit}>
<DynamicCustomTextarea
value={input}
onChange={handleInputChange}
placeholder="Enter text"
cb={(e) =>
// @ts-expect-error waiting for update from the libray maintainer link: https://github.com/vercel/ai/discussions/799
handleSubmit(e)
}
loading={isLoading}
rightSection={
!!input ? (
<ActionIcon
variant="subtle"
radius="lg"
onClick={() => setInput("")}
>
<MdClear size="20" />
</ActionIcon>
) : undefined
}
/>
<div className="w-full gap-2 pb-2 flex-row-between">
{/* //---------------------------------------------------CUSTOMIZATION */}
<CustomizationMenu selected={selected} setSelected={setSelected} />
<div className="gap-3 flex-row-start">
{isLoading ? (
<ActionIcon
type="button"
onClick={stop}
radius="lg"
size="xl"
variant="light"
>
<IoStopCircleOutline size="20" />
</ActionIcon>
) : (
<Button
loading={isLoading}
type="submit"
radius="lg"
w="fit-content"
disabled={!input}
>
Paraphrase
</Button>
)}
</div>
</div>
</form>
{/* //---------------------------------------------------OUTPUT AREA */}
<div
hidden={messages.length < 2}
className="mt-4 space-y-2 rounded-lg px-1 py-4"
>
{messages
.filter((msg) => msg.role === "assistant")
.reverse()
.map((msg, i) => (
<div
key={i}
className="flex items-start justify-between gap-2 rounded bg-slate-700/60 px-3 py-4 text-slate-200"
>
<Markdown>{msg.content}</Markdown>
<CopyButton text={msg.content} />
</div>
))}
{messages.length > 1 && (
<Button
variant="light"
opacity={isLoading ? 0 : 1}
color="red"
leftSection={<AiOutlineClear />}
onClick={() => {
setMessages([]);
setHistory([]);
}}
>
Clear
</Button>
)}
</div>
</ToolContainer>
);
export function Paraphraser() {
return <ParaphraserMain />;
}
16 changes: 15 additions & 1 deletion global.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type Database } from "./src/lib/db.types"
import { type Message } from "ai";
import { type Database } from "./src/types/db.types"

declare global {
type DB = Database
type LangPair = { lang: string; level: string }
Expand All @@ -8,5 +10,17 @@ declare global {
email: string
languages: LangPair[]
}
type Paraphraser = {
id: number;
user_id: string;
configs: Json | {
temperature: number;
tones: Tone[];
};
history: Message[] | Json;
created_at: string;
updated_at: string;
}
type PromiseType<T extends Promise<unknown>> = T extends Promise<infer U> ? U : never;
}

54 changes: 54 additions & 0 deletions src/actions/paraphraser/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { z } from "zod";

const authSupabase = async () => {
const supabase = createClient(cookies());
const { data, error } = await supabase.auth.getUser();
if (error) {
console.warn("Auth Error", error);
return redirect("/login");
}
return { supabase, id: data.user?.id as string };
};

export const getParaphraser = async (): Promise<Paraphraser | undefined> => {
const { supabase, id } = await authSupabase();
const { error, data } = await supabase
.from("paraphraser")
.select()
.eq("user_id", id);
if (!!error) {
throw Error(error.message);
}
return data[0];
};

const paraphraserSchema = z.object({
temperature: z.number(),
tones: z.array(
z.object({ id: z.string().optional(), label: z.string(), value: z.string() })
),
});
export const updateParaphraser = async (
inputs: Pick<Paraphraser, "configs">
) => {
const validate = paraphraserSchema.safeParse(inputs);
if (!validate.success) {
console.warn("actions:updateParaphraser", inputs);
throw Error(validate.error.message);
}
const { supabase, id } = await authSupabase();
const { data, error } = await supabase
.from("paraphraser")
.upsert(
{ user_id: id, configs: inputs, updated_at: new Date().toDateString() },
{ onConflict: "user_id" }
);
if (!!error) {
throw Error(error.message);
}
return data;
};
Loading