├── .dev.vars.example ├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── components.json ├── drizzle.config.ts ├── drizzle.do.config.ts ├── index.html ├── package-lock.json ├── package.json ├── public ├── chatsemble-app.jpg ├── logo.ico ├── logo.svg └── notion-avatars │ ├── avatar-01.svg │ ├── avatar-02.svg │ ├── avatar-03.svg │ ├── avatar-04.svg │ ├── avatar-05.svg │ ├── avatar-06.svg │ ├── avatar-07.svg │ ├── avatar-08.svg │ ├── avatar-09.svg │ └── avatar-10.svg ├── src ├── client │ ├── app.tsx │ ├── components │ │ ├── agents │ │ │ ├── agent-edit.tsx │ │ │ ├── agent-form.tsx │ │ │ ├── agent-placeholder.tsx │ │ │ ├── agent-skeleton.tsx │ │ │ ├── agents-sidebar.tsx │ │ │ └── new-agent-dialog.tsx │ │ ├── chat-room-member │ │ │ ├── chat-member-badge.tsx │ │ │ ├── chat-member-combobox.tsx │ │ │ ├── chat-member-list.tsx │ │ │ ├── chat-member-multi-select.tsx │ │ │ ├── chat-member-remove-button.tsx │ │ │ └── new │ │ │ │ ├── chat-member-add-dialog.tsx │ │ │ │ └── chat-member-add-form.tsx │ │ ├── chat-room │ │ │ ├── chat-room-message.tsx │ │ │ ├── chat-room-not-selected.tsx │ │ │ ├── chat-room.tsx │ │ │ ├── common │ │ │ │ └── chat-room-type-badge.tsx │ │ │ ├── details │ │ │ │ ├── chat-details-dialog.tsx │ │ │ │ ├── chat-details-members-section.tsx │ │ │ │ ├── chat-details-section.tsx │ │ │ │ └── chat-details-workflows-section.tsx │ │ │ ├── list │ │ │ │ └── chat-room-list.tsx │ │ │ ├── main │ │ │ │ ├── chat-room-main-display.tsx │ │ │ │ └── chat-room-main-header.tsx │ │ │ ├── new │ │ │ │ ├── new-chat-dialog.tsx │ │ │ │ └── new-chat-room-group.tsx │ │ │ └── thread │ │ │ │ ├── chat-room-thread-display.tsx │ │ │ │ └── chat-room-thread-header.tsx │ │ ├── common │ │ │ ├── avatar-picker.tsx │ │ │ ├── card-toggle-groups.tsx │ │ │ ├── confirmation-dialog.tsx │ │ │ ├── copy-button.tsx │ │ │ └── theme-toggle.tsx │ │ ├── icons │ │ │ ├── loading-pyramid.tsx │ │ │ └── logo-icon.tsx │ │ ├── layout │ │ │ ├── app-header.tsx │ │ │ ├── app-layout-skeleton.tsx │ │ │ ├── app-layout.tsx │ │ │ ├── app-nav-user.tsx │ │ │ ├── app-sidebar-group-skeleton.tsx │ │ │ └── app-sidebar.tsx │ │ ├── providers │ │ │ ├── auth-provider.tsx │ │ │ ├── organization-connection-provider.tsx │ │ │ ├── query-provider.tsx │ │ │ └── theme-provider.tsx │ │ ├── settings │ │ │ ├── invite-member-dialog.tsx │ │ │ ├── organization-form.tsx │ │ │ ├── profile-form.tsx │ │ │ └── settings-dialog.tsx │ │ ├── tools │ │ │ ├── annotated-tool.tsx │ │ │ ├── scheduled-workflow-tool.tsx │ │ │ └── sources-tool.tsx │ │ └── ui │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── chat-message-area.tsx │ │ │ ├── chat-message.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── markdown-content.tsx │ │ │ ├── popover.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── tiptap-chat-input.tsx │ │ │ ├── tiptap │ │ │ ├── mention-config.ts │ │ │ ├── mention-list.tsx │ │ │ ├── metion-plugin.ts │ │ │ ├── style.css │ │ │ └── tiptap.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ ├── tool-invocation.tsx │ │ │ └── tooltip.tsx │ ├── hooks │ │ ├── organization │ │ │ ├── use-main-chat-room-state.ts │ │ │ ├── use-organization-connection.ts │ │ │ ├── use-organization-state.ts │ │ │ └── use-thread-chat-room-state.ts │ │ ├── use-mobile.ts │ │ ├── use-scroll-to-bottom.ts │ │ ├── use-textarea-resize.ts │ │ └── use-web-socket.ts │ ├── index.css │ ├── lib │ │ ├── api-client.ts │ │ ├── auth-client.ts │ │ ├── chat.ts │ │ ├── date.tsx │ │ ├── id-parsing.ts │ │ └── utils.ts │ ├── main.tsx │ ├── routeTree.gen.ts │ ├── routes │ │ ├── (app) │ │ │ ├── agents.tsx │ │ │ ├── chat.tsx │ │ │ └── route.tsx │ │ ├── __root.tsx │ │ ├── auth │ │ │ ├── signin.tsx │ │ │ └── signup.tsx │ │ └── index.tsx │ ├── types │ │ └── auth.ts │ └── vite-env.d.ts ├── server │ ├── ai │ │ ├── prompts │ │ │ ├── agent │ │ │ │ ├── default-prompt.ts │ │ │ │ ├── prompt-parts.ts │ │ │ │ └── workflow-prompt.ts │ │ │ └── router-prompt.ts │ │ ├── tools │ │ │ ├── create-thread-tool.ts │ │ │ ├── deep-search-tool.ts │ │ │ ├── index.ts │ │ │ ├── schedule-workflow-tool.ts │ │ │ ├── web-crawler-tool.ts │ │ │ └── web-search-tool.ts │ │ └── utils │ │ │ ├── data-stream.ts │ │ │ └── message.ts │ ├── auth │ │ ├── index.ts │ │ └── organization-permissions.ts │ ├── db │ │ ├── index.ts │ │ ├── migrations │ │ │ ├── 0000_petite_norrin_radd.sql │ │ │ └── meta │ │ │ │ ├── 0000_snapshot.json │ │ │ │ └── _journal.json │ │ ├── schema │ │ │ ├── auth.ts │ │ │ └── index.ts │ │ └── seed.sql │ ├── email │ │ ├── index.ts │ │ └── templates │ │ │ ├── email-verification.tsx │ │ │ ├── organization-invitation.tsx │ │ │ └── password-reset.tsx │ ├── index.ts │ ├── middleware │ │ └── auth.ts │ ├── organization-do │ │ ├── agent.ts │ │ ├── chat-room.ts │ │ ├── db │ │ │ ├── migrations │ │ │ │ ├── 0000_medical_zzzax.sql │ │ │ │ ├── meta │ │ │ │ │ ├── 0000_snapshot.json │ │ │ │ │ └── _journal.json │ │ │ │ └── migrations.js │ │ │ ├── schema.ts │ │ │ └── services │ │ │ │ ├── agents.ts │ │ │ │ ├── chat-room-members.ts │ │ │ │ ├── chat-room-message.ts │ │ │ │ ├── chat-room.ts │ │ │ │ ├── index.ts │ │ │ │ └── workflow.ts │ │ ├── organization.ts │ │ └── workflow.ts │ ├── routes │ │ ├── index.ts │ │ ├── protected │ │ │ ├── agents.ts │ │ │ ├── chat │ │ │ │ ├── chat-room-members.ts │ │ │ │ ├── chat-room.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── organization-user.ts │ │ │ └── workflows.ts │ │ └── websocket │ │ │ └── organization.ts │ └── types │ │ ├── hono.ts │ │ └── session.ts └── shared │ ├── lib │ └── chat.ts │ └── types │ ├── agent.ts │ ├── chat-ws.ts │ ├── chat.ts │ ├── helper.ts │ ├── index.ts │ └── workflow.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.worker.json ├── vite.config.ts ├── worker-configuration.d.ts └── wrangler.jsonc /.dev.vars.example: -------------------------------------------------------------------------------- 1 | AI_GATEWAY_OPENAI_URL=https://gateway.ai.cloudflare.com/v1/43243242/chatsemble/openai 2 | AI_GATEWAY_GROQ_URL=https://gateway.ai.cloudflare.com/v1/432432342/chatsemble/groq 3 | 4 | OPENAI_API_KEY=key 5 | GROQ_API_KEY=key 6 | 7 | BRAVE_API_KEY=key 8 | FIRECRAWL_API_KEY=key 9 | 10 | APP_URL=http://localhost:5173 11 | 12 | BETTER_AUTH_SECRET=secret 13 | 14 | EMAIL_SENDER=sender@email.com 15 | RESEND_API_KEY=key 16 | MOCK_SEND_EMAIL=true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_APP_URL=http://localhost:5173 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .wrangler 27 | .dev.vars 28 | .env 29 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["./src/client/routeTree.gen.ts"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab", 15 | "indentWidth": 2, 16 | "lineWidth": 80 17 | }, 18 | "organizeImports": { 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "correctness": { 26 | "noUnusedFunctionParameters": "error", 27 | "noUnusedImports": "error", 28 | "noUnusedVariables": "error", 29 | "useHookAtTopLevel": "error" 30 | }, 31 | "style": { 32 | "useBlockStatements": "error", 33 | "useFilenamingConvention": { 34 | "level": "error", 35 | "options": { 36 | "strictCase": true, 37 | "filenameCases": ["kebab-case"] 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | "javascript": { 44 | "formatter": { 45 | "quoteStyle": "double", 46 | "trailingCommas": "all", 47 | "semicolons": "always", 48 | "arrowParentheses": "always" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/client/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@client/components", 15 | "utils": "@client/lib/utils", 16 | "ui": "@client/components/ui", 17 | "lib": "@client/lib", 18 | "hooks": "@client/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { defineConfig } from "drizzle-kit"; 3 | import path from "node:path"; 4 | import fs from "node:fs"; 5 | 6 | function getLocalD1DB() { 7 | try { 8 | const basePath = path.resolve("./.wrangler/state/v3/d1"); 9 | const dbFile = fs 10 | .readdirSync(basePath, { encoding: "utf-8", recursive: true }) 11 | .find((f) => f.endsWith(".sqlite")); 12 | 13 | if (!dbFile) { 14 | throw new Error(`.sqlite file not found in ${basePath}`); 15 | } 16 | 17 | const url = path.resolve(basePath, dbFile); 18 | return url; 19 | } catch (err) { 20 | console.error(err); 21 | 22 | return null; 23 | } 24 | } 25 | 26 | export default defineConfig({ 27 | out: "./src/server/db/migrations", 28 | schema: "./src/server/db/schema/index.ts", 29 | dialect: "sqlite", 30 | ...(process.env.NODE_ENV === "production" 31 | ? { 32 | driver: "d1-http", 33 | dbCredentials: { 34 | accountId: process.env.CLOUDFLARE_ACCOUNT_ID, 35 | databaseId: process.env.DATABASE_ID, 36 | token: process.env.CLOUDFLARE_API_TOKEN, 37 | }, 38 | } 39 | : { 40 | dbCredentials: { 41 | url: getLocalD1DB(), 42 | }, 43 | }), 44 | }); 45 | -------------------------------------------------------------------------------- /drizzle.do.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | const drizzleOut = process.env.DRIZZLE_OUT; 5 | const drizzleSchema = process.env.DRIZZLE_SCHEMA; 6 | 7 | // For Durable Objects 8 | export default defineConfig({ 9 | out: drizzleOut, 10 | schema: drizzleSchema, 11 | dialect: "sqlite", 12 | driver: "durable-sqlite", 13 | }); 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Chatsemble 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/chatsemble-app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alwurts/Chatsemble/2221748cebce401c466a786e77d2cebf9ca40ff9/public/chatsemble-app.jpg -------------------------------------------------------------------------------- /public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alwurts/Chatsemble/2221748cebce401c466a786e77d2cebf9ca40ff9/public/logo.ico -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 14 | 16 | 18 | 20 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-03.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 13 | 15 | 17 | 19 | 21 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-07.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 14 | 16 | 17 | 19 | 21 | 23 | 25 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-08.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 14 | 15 | 17 | 19 | 21 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-09.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | -------------------------------------------------------------------------------- /public/notion-avatars/avatar-10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 12 | 14 | 16 | 18 | 20 | -------------------------------------------------------------------------------- /src/client/app.tsx: -------------------------------------------------------------------------------- 1 | function App() { 2 | return ( 3 |
4 | Hello world 5 |
6 | ); 7 | } 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /src/client/components/agents/agent-edit.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AgentNotFound } from "@client/components/agents/agent-placeholder"; 4 | 5 | import { honoClient } from "@client/lib/api-client"; 6 | import { useQuery } from "@tanstack/react-query"; 7 | import { AgentSkeleton } from "./agent-skeleton"; 8 | 9 | import { zodResolver } from "@hookform/resolvers/zod"; 10 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 11 | import { useForm } from "react-hook-form"; 12 | 13 | import { Form } from "@client/components/ui/form"; 14 | import { type AgentFormValues, createAgentSchema } from "@shared/types"; 15 | import { Bot } from "lucide-react"; 16 | import { 17 | AppHeader, 18 | AppHeaderIcon, 19 | AppHeaderSeparator, 20 | AppHeaderTitle, 21 | } from "../layout/app-header"; 22 | 23 | import { toast } from "sonner"; 24 | import { Button } from "../ui/button"; 25 | import { AgentForm } from "./agent-form"; 26 | 27 | export function AgentEdit({ agentId }: { agentId: string }) { 28 | const { data: agent, isLoading } = useQuery({ 29 | queryKey: ["agent", agentId], 30 | queryFn: async () => { 31 | const response = await honoClient.api.agents[":id"].$get({ 32 | param: { id: agentId }, 33 | }); 34 | const agent = await response.json(); 35 | return agent; 36 | }, 37 | }); 38 | 39 | const queryClient = useQueryClient(); 40 | 41 | const form = useForm({ 42 | resolver: zodResolver(createAgentSchema), 43 | values: { 44 | name: agent?.name ?? "", 45 | image: agent?.image ?? "", 46 | description: agent?.description ?? "", 47 | tone: agent?.tone ?? "formal", 48 | verbosity: agent?.verbosity ?? "concise", 49 | emojiUsage: agent?.emojiUsage ?? "none", 50 | languageStyle: agent?.languageStyle ?? "simple", 51 | }, 52 | }); 53 | 54 | const updateAgentMutation = useMutation({ 55 | mutationFn: async (values: AgentFormValues) => { 56 | const response = await honoClient.api.agents[":id"].$put({ 57 | param: { id: agentId }, 58 | json: values, 59 | }); 60 | return response.json(); 61 | }, 62 | onSuccess: () => { 63 | queryClient.invalidateQueries({ queryKey: ["agents"] }); 64 | queryClient.invalidateQueries({ queryKey: ["agent", agentId] }); 65 | toast.success("Agent updated successfully"); 66 | }, 67 | }); 68 | 69 | const onSubmit = (values: AgentFormValues) => { 70 | updateAgentMutation.mutate(values); 71 | }; 72 | 73 | return ( 74 |
75 | 79 | 80 | 81 | 82 | 83 | 84 | Edit Agent 85 | 88 | 89 |
90 |
91 | {isLoading ? ( 92 | 93 | ) : agent ? ( 94 | 95 | ) : ( 96 | 97 | )} 98 |
99 |
100 |
101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/client/components/agents/agent-placeholder.tsx: -------------------------------------------------------------------------------- 1 | export function AgentNotSelected() { 2 | return ( 3 |
4 | No agent selected 5 |

6 | Please select an agent from the sidebar 7 |

8 |
9 | ); 10 | } 11 | 12 | export function AgentNotFound() { 13 | return ( 14 |
15 | Agent not found 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/client/components/agents/agent-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@client/components/ui/skeleton"; 2 | 3 | export function AgentSkeleton() { 4 | return ( 5 |
6 | 7 | 8 |
9 |
10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 |

26 | 27 |

28 | 29 |
30 |
31 | 32 |
33 | {Array.from({ length: 4 }).map((_, i) => ( 34 | // biome-ignore lint/suspicious/noArrayIndexKey: 35 | 36 | ))} 37 |
38 | 39 |
40 |
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/client/components/agents/new-agent-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from "@hookform/resolvers/zod"; 2 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 3 | import { useForm } from "react-hook-form"; 4 | 5 | import { Button } from "@client/components/ui/button"; 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogDescription, 10 | DialogFooter, 11 | DialogHeader, 12 | DialogTitle, 13 | } from "@client/components/ui/dialog"; 14 | import { Form } from "@client/components/ui/form"; 15 | import { honoClient } from "@client/lib/api-client"; 16 | import { type AgentFormValues, createAgentSchema } from "@shared/types"; 17 | import { useRouter } from "@tanstack/react-router"; 18 | import { AgentForm } from "./agent-form"; 19 | 20 | export function NewAgentDialog({ 21 | open, 22 | setOpen, 23 | }: { 24 | open: boolean; 25 | setOpen: (open: boolean) => void; 26 | }) { 27 | return ( 28 | 29 | 30 | {open && } 31 | 32 | 33 | ); 34 | } 35 | 36 | function NewAgentDialogContent({ 37 | setOpen, 38 | }: { 39 | setOpen: (open: boolean) => void; 40 | }) { 41 | const router = useRouter(); 42 | const queryClient = useQueryClient(); 43 | const form = useForm({ 44 | resolver: zodResolver(createAgentSchema), 45 | defaultValues: { 46 | name: "", 47 | image: "/notion-avatars/avatar-01.svg", 48 | description: "", 49 | tone: "formal", 50 | verbosity: "concise", 51 | emojiUsage: "occasional", 52 | languageStyle: "simple", 53 | }, 54 | }); 55 | 56 | const createChatMutation = useMutation({ 57 | mutationFn: async (values: AgentFormValues) => { 58 | const response = await honoClient.api.agents.$post({ 59 | json: values, 60 | }); 61 | const data = await response.json(); 62 | return data; 63 | }, 64 | onSuccess: (data) => { 65 | queryClient.invalidateQueries({ queryKey: ["agents"] }); 66 | router.navigate({ 67 | to: "/agents", 68 | search: (prev) => ({ 69 | ...prev, 70 | agentId: data.agentId, 71 | }), 72 | }); 73 | setOpen(false); 74 | }, 75 | }); 76 | 77 | const onSubmit = (values: AgentFormValues) => { 78 | createChatMutation.mutate(values); 79 | }; 80 | 81 | return ( 82 | <> 83 | 84 | Create New Agent 85 | 86 | Create a new agent to start conversations with your team. 87 | 88 | 89 |
90 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/client/components/chat-room-member/chat-member-badge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@client/components/ui/badge"; 2 | import { cn } from "@client/lib/utils"; 3 | import type { ChatRoomMemberType } from "@shared/types"; 4 | 5 | interface ChatMemberBadgeProps { 6 | type: ChatRoomMemberType; 7 | } 8 | 9 | export function ChatMemberBadge({ type }: ChatMemberBadgeProps) { 10 | return ( 11 | 18 | {type === "user" ? "User" : "Agent"} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/client/components/chat-room-member/chat-member-list.tsx: -------------------------------------------------------------------------------- 1 | import { ChatMemberBadge } from "@client/components/chat-room-member/chat-member-badge"; 2 | import { ChatMemberRemoveButton } from "@client/components/chat-room-member/chat-member-remove-button"; 3 | import { ChatMemberAddDialog } from "@client/components/chat-room-member/new/chat-member-add-dialog"; 4 | import { useOrganizationConnectionContext } from "@client/components/providers/organization-connection-provider"; 5 | import { 6 | Avatar, 7 | AvatarFallback, 8 | AvatarImage, 9 | } from "@client/components/ui/avatar"; 10 | import { authClient } from "@client/lib/auth-client"; 11 | import type { ChatRoomMember } from "@shared/types"; 12 | 13 | interface ChatMemberListProps { 14 | members: ChatRoomMember[]; 15 | showRemoveButton?: boolean; 16 | } 17 | 18 | export function ChatMemberList({ 19 | members, 20 | showRemoveButton = false, 21 | }: ChatMemberListProps) { 22 | const { 23 | mainChatRoomState: { room }, 24 | } = useOrganizationConnectionContext(); 25 | 26 | const { data: session, isPending: isSessionPending } = 27 | authClient.useSession(); 28 | 29 | if (!members || members.length === 0) { 30 | return ; 31 | } 32 | 33 | return ( 34 |
35 | 36 | {members.map((member: ChatRoomMember) => ( 37 |
41 |
42 | 43 | 44 | 45 | {member.name[0]?.toUpperCase() ?? "?"} 46 | 47 | 48 | {member.name} 49 | 50 |
51 | {showRemoveButton && 52 | room && 53 | !isSessionPending && 54 | session?.user.id !== member.id && ( 55 | 56 | )} 57 |
58 | ))} 59 |
60 | ); 61 | } 62 | 63 | function ChatMemberListEmpty() { 64 | return ( 65 |
66 | No members in this chat room 67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/client/components/chat-room-member/chat-member-remove-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@client/components/ui/button"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle, 11 | DialogTrigger, 12 | } from "@client/components/ui/dialog"; 13 | import { honoClient } from "@client/lib/api-client"; 14 | import type { ChatRoomMember } from "@shared/types"; 15 | import { useMutation } from "@tanstack/react-query"; 16 | import { Trash2 } from "lucide-react"; 17 | import { useState } from "react"; 18 | 19 | interface ChatMemberRemoveButtonProps { 20 | member: ChatRoomMember; 21 | roomId: string; 22 | } 23 | 24 | export function ChatMemberRemoveButton({ 25 | member, 26 | roomId, 27 | }: ChatMemberRemoveButtonProps) { 28 | const [open, setOpen] = useState(false); 29 | 30 | const removeMemberMutation = useMutation({ 31 | mutationFn: async () => { 32 | const response = await honoClient.api.chat["chat-rooms"][ 33 | ":chatRoomId" 34 | ].members[":memberId"].$delete({ 35 | param: { 36 | chatRoomId: roomId, 37 | memberId: member.id, 38 | }, 39 | }); 40 | return response.json(); 41 | }, 42 | onSuccess: () => { 43 | setOpen(false); 44 | }, 45 | }); 46 | 47 | return ( 48 | 49 | 50 | 58 | 59 | 60 | 61 | Remove Member 62 | 63 | Are you sure you want to remove {member.name} from this chat room? 64 | 65 | 66 | 67 | 70 | 77 | 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/client/components/chat-room-member/new/chat-member-add-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { ChatMemberAddForm } from "@client/components/chat-room-member/new/chat-member-add-form"; 2 | import { useOrganizationConnectionContext } from "@client/components/providers/organization-connection-provider"; 3 | import { Button } from "@client/components/ui/button"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogTrigger, 8 | } from "@client/components/ui/dialog"; 9 | import { Plus } from "lucide-react"; 10 | import { useState } from "react"; 11 | 12 | export function ChatMemberAddDialog() { 13 | const [open, setOpen] = useState(false); 14 | 15 | return ( 16 | 17 | 18 | 22 | 23 | 24 | setOpen(false)} /> 25 | 26 | 27 | ); 28 | } 29 | 30 | function ChatMemberAddDialogContent({ 31 | onSuccess, 32 | }: { 33 | onSuccess: () => void; 34 | }) { 35 | const { 36 | mainChatRoomState: { room }, 37 | } = useOrganizationConnectionContext(); 38 | 39 | if (!room) { 40 | return null; 41 | } 42 | 43 | return ; 44 | } 45 | -------------------------------------------------------------------------------- /src/client/components/chat-room/chat-room-not-selected.tsx: -------------------------------------------------------------------------------- 1 | import { AppHeader, AppHeaderIcon } from "@client/components/layout/app-header"; 2 | import { MessagesSquare } from "lucide-react"; 3 | 4 | export function ChatRoomNotSelected() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

No chat room selected

15 |

16 | Select a chat room from the sidebar to start chatting 17 |

18 |
19 |
20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/client/components/chat-room/chat-room.tsx: -------------------------------------------------------------------------------- 1 | import { ChatRoomNotSelected } from "@client/components/chat-room/chat-room-not-selected"; 2 | import { ChatRoomMainDisplay } from "@client/components/chat-room/main/chat-room-main-display"; 3 | import { ChatRoomMainHeader } from "@client/components/chat-room/main/chat-room-main-header"; 4 | import { ChatRoomThreadHeader } from "@client/components/chat-room/thread/chat-room-thread-header"; 5 | import { 6 | ResizableHandle, 7 | ResizablePanel, 8 | ResizablePanelGroup, 9 | } from "@client/components/ui/resizable"; 10 | import { useSearch } from "@tanstack/react-router"; 11 | import { ChatRoomThreadDisplay } from "./thread/chat-room-thread-display"; 12 | 13 | export function ChatRoom() { 14 | const { roomId, threadId } = useSearch({ strict: false }); 15 | 16 | if (!roomId) { 17 | return ; 18 | } 19 | 20 | return ( 21 | <> 22 | 23 | 29 |
30 | 31 | 32 |
33 |
34 | {threadId && } 35 | {threadId && ( 36 | 42 |
43 | 44 | 45 |
46 |
47 | )} 48 |
49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/client/components/chat-room/common/chat-room-type-badge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@client/components/ui/badge"; 2 | import { cn } from "@client/lib/utils"; 3 | import type { ChatRoomType } from "@shared/types"; 4 | 5 | interface ChatRoomTypeBadgeProps { 6 | type: ChatRoomType; 7 | label?: string; 8 | } 9 | 10 | export function ChatRoomTypeBadge({ type, label }: ChatRoomTypeBadgeProps) { 11 | const labels: Record = { 12 | public: "Public", 13 | }; 14 | return ( 15 | 22 | {label ?? labels[type]} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/client/components/chat-room/details/chat-details-members-section.tsx: -------------------------------------------------------------------------------- 1 | import { ChatMemberList } from "@client/components/chat-room-member/chat-member-list"; 2 | import { useOrganizationConnectionContext } from "@client/components/providers/organization-connection-provider"; 3 | 4 | export function ChatDetailsMembersSection() { 5 | const { 6 | mainChatRoomState: { members }, 7 | } = useOrganizationConnectionContext(); 8 | 9 | // TODO: Show or hide the add and remove member button based on the user's permissions 10 | 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /src/client/components/chat-room/details/chat-details-section.tsx: -------------------------------------------------------------------------------- 1 | import { ChatRoomTypeBadge } from "@client/components/chat-room/common/chat-room-type-badge"; 2 | import { useOrganizationConnectionContext } from "@client/components/providers/organization-connection-provider"; 3 | import { Separator } from "@client/components/ui/separator"; 4 | import { CalendarIcon } from "lucide-react"; 5 | import { UsersIcon } from "lucide-react"; 6 | 7 | export function ChatDetailsSection() { 8 | const { 9 | mainChatRoomState: { room, members }, 10 | } = useOrganizationConnectionContext(); 11 | 12 | if (!room) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
18 |
19 |

{room.name}

20 | 21 |
22 | 23 | 24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |

Created

32 |

33 | {new Date(room.createdAt).toLocaleDateString("en-US", { 34 | year: "numeric", 35 | month: "long", 36 | day: "numeric", 37 | })} 38 |

39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 |

Members

48 |

{members.length || 0}

49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/client/components/chat-room/new/new-chat-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import type { z } from "zod"; 3 | 4 | import { NewChatRoomGroupForm } from "@client/components/chat-room/new/new-chat-room-group"; 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogDescription, 9 | DialogHeader, 10 | DialogTitle, 11 | } from "@client/components/ui/dialog"; 12 | import { honoClient } from "@client/lib/api-client"; 13 | import type { ChatRoomType, createChatRoomSchema } from "@shared/types"; 14 | import { useRouter } from "@tanstack/react-router"; 15 | import type { Dispatch, SetStateAction } from "react"; 16 | import { toast } from "sonner"; 17 | 18 | type CreateChatRoomFormValues = z.infer; 19 | 20 | export type NewChatDialogState = { 21 | type: ChatRoomType; 22 | } | null; 23 | 24 | type NewGroupChatDialogProps = { 25 | dialogState: NewChatDialogState; 26 | setDialogState: Dispatch>; 27 | }; 28 | 29 | export function NewChatRoomDialog({ 30 | dialogState, 31 | setDialogState, 32 | }: NewGroupChatDialogProps) { 33 | return ( 34 | { 37 | if (!open) { 38 | setDialogState(null); 39 | } 40 | }} 41 | > 42 | 43 | {dialogState && ( 44 | 48 | )} 49 | 50 | 51 | ); 52 | } 53 | 54 | function NewChatRoomDialogContent({ 55 | dialogState, 56 | setDialogState, 57 | }: NewGroupChatDialogProps & { 58 | dialogState: NonNullable; 59 | }) { 60 | const router = useRouter(); 61 | 62 | const createChatMutation = useMutation({ 63 | mutationFn: async (values: CreateChatRoomFormValues) => { 64 | const response = await honoClient.api.chat["chat-rooms"].$post({ 65 | json: values, 66 | }); 67 | const data = await response.json(); 68 | if ("error" in data) { 69 | throw new Error(data.error); 70 | } 71 | return data; 72 | }, 73 | onSuccess: (data) => { 74 | router.navigate({ 75 | to: "/chat", 76 | search: { roomId: data.roomId }, 77 | }); 78 | setDialogState(null); 79 | toast.success("Chat room created successfully"); 80 | }, 81 | onError: (error) => { 82 | console.error(error); 83 | toast.error("Failed to create chat room"); 84 | }, 85 | }); 86 | 87 | const onSubmit = (values: CreateChatRoomFormValues) => { 88 | createChatMutation.mutate(values); 89 | }; 90 | 91 | return ( 92 | <> 93 | 94 | Create New Chat Room 95 | 96 | Create a new chat room to start conversations with your team. 97 | 98 | 99 | 100 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/client/components/chat-room/thread/chat-room-thread-display.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ChatMessageSkeleton, 5 | ChatRoomMessage, 6 | } from "@client/components/chat-room/chat-room-message"; 7 | import { ChatMessagesSkeleton } from "@client/components/chat-room/chat-room-message"; 8 | import { useAuthSession } from "@client/components/providers/auth-provider"; 9 | import { useOrganizationConnectionContext } from "@client/components/providers/organization-connection-provider"; 10 | import { ChatMessageArea } from "@client/components/ui/chat-message-area"; 11 | import { Separator } from "@client/components/ui/separator"; 12 | import { 13 | ChatInput, 14 | ChatInputSubmit, 15 | ChatInputTiptap, 16 | } from "@client/components/ui/tiptap-chat-input"; 17 | import type { ChatInputValue } from "@shared/types"; 18 | import { useMemo } from "react"; 19 | 20 | export function ChatRoomThreadDisplay() { 21 | const { 22 | mainChatRoomState: { members }, 23 | chatRoomThreadState: { handleSubmit, messages, status, threadMessage }, 24 | connectionStatus, 25 | } = useOrganizationConnectionContext(); 26 | 27 | const { user } = useAuthSession(); 28 | 29 | const isLoading = connectionStatus !== "connected" || status !== "success"; 30 | 31 | const membersWithoutCurrentUser = useMemo( 32 | () => members.filter((member) => member.id !== user.id), 33 | [members, user.id], 34 | ); 35 | 36 | const onSubmit = (value: ChatInputValue) => { 37 | handleSubmit({ value }); 38 | }; 39 | 40 | return ( 41 |
42 | 46 |
47 |
48 | {threadMessage ? ( 49 | 50 | ) : ( 51 | isLoading && 52 | )} 53 |
54 | 55 |
56 | {isLoading ? ( 57 | 58 | ) : messages.length > 0 ? ( 59 | messages.map((message) => ( 60 | 61 | )) 62 | ) : ( 63 |
64 | Send a message to start the thread 65 |
66 | )} 67 |
68 |
69 |
70 |
71 | 76 | 77 | 78 | 79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/client/components/chat-room/thread/chat-room-thread-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@client/components/ui/button"; 4 | import { Separator } from "@client/components/ui/separator"; 5 | import { useRouter } from "@tanstack/react-router"; 6 | import { X } from "lucide-react"; 7 | 8 | export function ChatRoomThreadHeader() { 9 | const router = useRouter(); 10 | 11 | return ( 12 |
13 |
14 | 31 | 35 | Thread 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/client/components/common/avatar-picker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@client/components/ui/button"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "@client/components/ui/dialog"; 10 | import { UserRound } from "lucide-react"; 11 | import { useState } from "react"; 12 | 13 | interface AvatarPickerProps { 14 | value: string; 15 | onChange: (value: string) => void; 16 | } 17 | 18 | export function AvatarPicker({ value, onChange }: AvatarPickerProps) { 19 | const [dialogOpen, setDialogOpen] = useState(false); 20 | 21 | return ( 22 |
23 |
24 | {value ? ( 25 | Agent avatar 30 | ) : ( 31 |
32 | 33 |
34 | )} 35 |
36 | 44 | 45 | 46 | 47 | Select Avatar 48 | 49 |
50 | {Array.from({ length: 10 }, (_, i) => i + 1).map((num) => { 51 | const avatarId = `/notion-avatars/avatar-${num.toString().padStart(2, "0")}.svg`; 52 | return ( 53 | 72 | ); 73 | })} 74 |
75 |
76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/client/components/common/card-toggle-groups.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ToggleGroup, 3 | ToggleGroupItem, 4 | } from "@client/components/ui/toggle-group"; 5 | import { idToReadableText } from "@client/lib/id-parsing"; 6 | 7 | interface ToggleGroupProps { 8 | value: T | undefined; 9 | options: Readonly; 10 | descriptions: Readonly>; 11 | iconMap: Readonly< 12 | Record> 13 | >; 14 | onValueChange: (value: T) => void; 15 | } 16 | 17 | export function CardToggleGroup({ 18 | value, 19 | onValueChange, 20 | options, 21 | descriptions, 22 | iconMap, 23 | }: ToggleGroupProps) { 24 | return ( 25 | 31 | {options.map((option) => { 32 | const Icon = iconMap[option] ?? (() => null); 33 | return ( 34 | 40 | 41 | {idToReadableText(option, { capitalize: true })} 42 |

43 | {descriptions[option]} 44 |

45 |
46 | ); 47 | })} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/client/components/common/confirmation-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertDialog, 3 | AlertDialogAction, 4 | AlertDialogCancel, 5 | AlertDialogContent, 6 | AlertDialogDescription, 7 | AlertDialogFooter, 8 | AlertDialogHeader, 9 | AlertDialogTitle, 10 | AlertDialogTrigger, 11 | } from "@client/components/ui/alert-dialog"; 12 | 13 | export function ConfirmationDialog({ 14 | children, 15 | title, 16 | description, 17 | open, 18 | onOpenChange, 19 | onConfirm, 20 | }: { 21 | children: React.ReactNode; 22 | title: string; 23 | description: string; 24 | open: boolean; 25 | onOpenChange: (open: boolean) => void; 26 | onConfirm: () => void; 27 | }) { 28 | return ( 29 | 30 | {children} 31 | 32 | 33 | {title} 34 | {description} 35 | 36 | 37 | Cancel 38 | Continue 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/client/components/common/copy-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@client/components/ui/button"; 2 | import { useState } from "react"; 3 | 4 | export const CopyButton = ({ textToCopy }: { textToCopy: string }) => { 5 | const [copied, setCopied] = useState(false); 6 | 7 | const handleCopy = async () => { 8 | await navigator.clipboard.writeText(textToCopy); 9 | setCopied(true); 10 | setTimeout(() => setCopied(false), 2000); 11 | }; 12 | 13 | return ( 14 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/client/components/common/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MoonIcon, SunIcon } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | import { useCallback } from "react"; 6 | import { Button } from "../ui/button"; 7 | import { SidebarMenuButton } from "../ui/sidebar"; 8 | 9 | export const ThemeToggle = ({ 10 | variant = "default", 11 | }: { variant?: "default" | "icon" }) => { 12 | const { theme, setTheme } = useTheme(); 13 | 14 | const toggleTheme = useCallback(() => { 15 | setTheme(theme === "light" ? "dark" : "light"); 16 | }, [theme, setTheme]); 17 | 18 | if (variant === "icon") { 19 | return ( 20 | 25 | ); 26 | } 27 | 28 | return ( 29 | 30 | 31 | 32 | Toggle theme 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/client/components/icons/loading-pyramid.tsx: -------------------------------------------------------------------------------- 1 | interface AnimatedPyramidIconProps 2 | extends Omit< 3 | React.SVGProps, 4 | "width" | "height" | "viewBox" | "color" 5 | > { 6 | // Exclude color prop as it's internally set 7 | /** Animation duration in seconds (e.g., "3s") */ 8 | duration?: string; 9 | /** Radius of the spheres */ 10 | sphereRadius?: number; 11 | } 12 | 13 | /** 14 | * An animated SVG icon showing 3 spheres in a pyramid formation, 15 | * cycling through positions and using the current CSS color. 16 | * Size is controlled externally via CSS/Tailwind classes. 17 | */ 18 | export function AnimatedPyramidIcon({ 19 | duration = "3s", 20 | sphereRadius = 20, 21 | ...props 22 | }: AnimatedPyramidIconProps) { 23 | const topPos = { x: 50, y: sphereRadius }; 24 | const bottomLeftPos = { x: sphereRadius, y: 100 - sphereRadius }; 25 | const bottomRightPos = { x: 100 - sphereRadius, y: 100 - sphereRadius }; 26 | 27 | const path1to2 = `M 0 0 L ${bottomLeftPos.x - topPos.x} ${bottomLeftPos.y - topPos.y}`; 28 | const path2to3 = `M 0 0 L ${bottomRightPos.x - bottomLeftPos.x} ${bottomRightPos.y - bottomLeftPos.y}`; 29 | const path3to1 = `M 0 0 L ${topPos.x - bottomRightPos.x} ${topPos.y - bottomRightPos.y}`; 30 | 31 | return ( 32 | 40 | Loading Pyramid 41 | 42 | 43 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/client/components/icons/logo-icon.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function LogoIcon(props: SVGProps) { 4 | return ( 5 | 13 | Chatsemble 14 | {/* */} 15 | 20 | 26 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/client/components/layout/app-header.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from "@client/components/ui/separator"; 2 | import { SidebarTrigger } from "@client/components/ui/sidebar"; 3 | import { cn } from "@client/lib/utils"; 4 | 5 | export function AppHeader({ children }: React.ComponentProps<"div">) { 6 | return ( 7 |
8 |
9 | 10 | 11 | {children} 12 |
13 |
14 | ); 15 | } 16 | 17 | export function AppHeaderIcon({ 18 | children, 19 | className, 20 | }: React.ComponentProps<"span">) { 21 | return ( 22 | 28 | {children} 29 | 30 | ); 31 | } 32 | 33 | export function AppHeaderTitle({ 34 | children, 35 | className, 36 | }: React.ComponentProps<"span">) { 37 | return ( 38 | {children} 39 | ); 40 | } 41 | 42 | export function AppHeaderSeparator({ className }: React.ComponentProps<"div">) { 43 | return ( 44 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/client/components/layout/app-layout-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarInset } from "@client/components/ui/sidebar"; 2 | import { Skeleton } from "@client/components/ui/skeleton"; 3 | 4 | function SkeletonSidebar() { 5 | return ( 6 |
10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | {Array.from({ length: 5 }).map((_, i) => ( 18 | 21 | i 22 | }`} 23 | className="h-8 w-full mb-2" 24 | /> 25 | ))} 26 |
27 |
28 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export function AppLayoutSkeleton() { 35 | return ( 36 |
37 |
38 | 39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/client/components/layout/app-layout.tsx: -------------------------------------------------------------------------------- 1 | import { AppSidebar } from "@client/components/layout/app-sidebar"; 2 | import { SidebarInset } from "@client/components/ui/sidebar"; 3 | 4 | export function AppLayout({ 5 | children, 6 | sidebarChildren, 7 | }: { 8 | children: React.ReactNode; 9 | sidebarChildren: React.ReactNode; 10 | }) { 11 | return ( 12 | <> 13 | {sidebarChildren} 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/layout/app-sidebar-group-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SidebarGroup, 3 | SidebarGroupAction, 4 | SidebarGroupLabel, 5 | SidebarMenu, 6 | SidebarMenuItem, 7 | SidebarMenuSkeleton, 8 | } from "@client/components/ui/sidebar"; 9 | import { Skeleton } from "@client/components/ui/skeleton"; 10 | 11 | export function AppSidebarGroupSkeleton({ 12 | listLength, 13 | }: { listLength: number }) { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {Array.from({ length: listLength }).map((_, index) => ( 24 | // biome-ignore lint/suspicious/noArrayIndexKey: 25 | 26 | 27 | 28 | ))} 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/client/components/providers/auth-provider.tsx: -------------------------------------------------------------------------------- 1 | import { authClient } from "@client/lib/auth-client"; 2 | import { createContext, useContext } from "react"; 3 | 4 | type SessionContextType = ReturnType["data"]; 5 | 6 | const SessionContext = createContext(null); 7 | 8 | export function useAuthSession() { 9 | const session = useContext(SessionContext); 10 | 11 | if (!session) { 12 | throw new Error("useSession must be used within an AuthProvider"); 13 | } 14 | 15 | return session; 16 | } 17 | 18 | export function AuthProvider({ children }: { children: React.ReactNode }) { 19 | const { data: session } = authClient.useSession(); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/client/components/providers/organization-connection-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | type UseOrganizationConnectionProps, 5 | useOrganizationConnection, 6 | } from "@client/hooks/organization/use-organization-connection"; 7 | 8 | import { type ReactNode, createContext, useContext } from "react"; 9 | 10 | const OrganizationConnectionContext = createContext | null>(null); 13 | 14 | interface OrganizationConnectionProviderProps 15 | extends UseOrganizationConnectionProps { 16 | children: ReactNode; 17 | } 18 | 19 | export function OrganizationConnectionProvider({ 20 | children, 21 | ...props 22 | }: OrganizationConnectionProviderProps) { 23 | const connectionState = useOrganizationConnection(props); 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | } 31 | 32 | export function useOrganizationConnectionContext() { 33 | const context = useContext(OrganizationConnectionContext); 34 | if (!context) { 35 | throw new Error( 36 | "useOrganizationConnectionContext must be used within a OrganizationConnectionProvider", 37 | ); 38 | } 39 | return context; 40 | } 41 | -------------------------------------------------------------------------------- /src/client/components/providers/query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import type { ReactNode } from "react"; 5 | 6 | const queryClient = new QueryClient(); 7 | 8 | export function QueryProvider({ children }: { children: ReactNode }) { 9 | return ( 10 | {children} 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/client/components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ThemeProvider as NextThemesProvider, 5 | type ThemeProviderProps, 6 | } from "next-themes"; 7 | 8 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/components/settings/profile-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useForm } from "react-hook-form"; 5 | import * as z from "zod"; 6 | 7 | import { Button } from "@client/components/ui/button"; 8 | import { 9 | Form, 10 | FormControl, 11 | FormDescription, 12 | FormField, 13 | FormItem, 14 | FormLabel, 15 | FormMessage, 16 | } from "@client/components/ui/form"; 17 | import { Input } from "@client/components/ui/input"; 18 | import { toast } from "sonner"; 19 | 20 | const profileFormSchema = z.object({ 21 | username: z 22 | .string() 23 | .min(2, { 24 | message: "Username must be at least 2 characters.", 25 | }) 26 | .max(30, { 27 | message: "Username must not be longer than 30 characters.", 28 | }), 29 | email: z 30 | .string({ 31 | required_error: "Please select an email to display.", 32 | }) 33 | .email(), 34 | }); 35 | 36 | type ProfileFormValues = z.infer; 37 | 38 | // TODO: Implement profile editing 39 | const defaultValues: Partial = { 40 | username: "john-doe", 41 | email: "john@example.com", 42 | }; 43 | 44 | export function ProfileForm() { 45 | const form = useForm({ 46 | resolver: zodResolver(profileFormSchema), 47 | defaultValues, 48 | mode: "onChange", 49 | }); 50 | 51 | function onSubmit() { 52 | toast.success("You submitted the following values:"); 53 | } 54 | 55 | return ( 56 |
57 | 58 | ( 62 | 63 | Username 64 | 65 | 66 | 67 | 68 | This is your public display name. It can be your real name or a 69 | pseudonym. 70 | 71 | 72 | 73 | )} 74 | /> 75 | ( 79 | 80 | Email 81 | 82 | 83 | 84 | 85 | You can manage verified email addresses in your email settings. 86 | 87 | 88 | 89 | )} 90 | /> 91 | 92 | 93 | 94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/client/components/tools/sources-tool.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollArea } from "@client/components/ui/scroll-area"; 2 | import { cn } from "@client/lib/utils"; 3 | import type { ToolSource } from "@shared/types"; 4 | import { Link2 } from "lucide-react"; 5 | 6 | export interface ToolInvocationSourceProps { 7 | source: ToolSource; 8 | } 9 | 10 | export function ToolInvocationSource({ source }: ToolInvocationSourceProps) { 11 | return ( 12 | 18 | {source.icon ? ( 19 | 20 | ) : ( 21 | 22 | )} 23 |
24 |
25 | {source.title} 26 |
27 | 28 |
34 | {source.url} 35 |
36 | 37 |
38 | {source.content} 39 |
40 |
41 |
42 | ); 43 | } 44 | 45 | export interface ToolInvocationSourcesListProps { 46 | sources: ToolSource[]; 47 | maxVisible?: number; 48 | maxHeight?: string; 49 | } 50 | 51 | export function ToolInvocationSourcesList({ 52 | sources, 53 | }: ToolInvocationSourcesListProps) { 54 | return ( 55 |
56 |
Sources ({sources.length})
57 | 58 |
59 | {sources.map((source, index) => ( 60 | 64 | ))} 65 |
66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/client/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from "class-variance-authority"; 2 | import type * as React from "react"; 3 | 4 | import { cn } from "@client/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-card text-card-foreground", 12 | destructive: 13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }, 20 | ); 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ); 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ); 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ); 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription }; 67 | -------------------------------------------------------------------------------- /src/client/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 2 | import type * as React from "react"; 3 | 4 | import { cn } from "@client/lib/utils"; 5 | 6 | function Avatar({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | function AvatarImage({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 32 | ); 33 | } 34 | 35 | function AvatarFallback({ 36 | className, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 48 | ); 49 | } 50 | 51 | export { Avatar, AvatarImage, AvatarFallback }; 52 | -------------------------------------------------------------------------------- /src/client/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import type * as React from "react"; 4 | 5 | import { cn } from "@client/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | }, 26 | ); 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span"; 36 | 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | export { Badge, badgeVariants }; 47 | -------------------------------------------------------------------------------- /src/client/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { ChevronRight, MoreHorizontal } from "lucide-react"; 3 | import type * as React from "react"; 4 | 5 | import { cn } from "@client/lib/utils"; 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return