From e2bf6e34b0b85c31065f52fb98b47b650af6a1ea Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Thu, 13 Feb 2025 14:05:40 +0000 Subject: [PATCH] feat: metadata (+ translations) for all pages --- apps/app/package.json | 48 +- .../actions/ai/assistant-settings-action.ts | 30 - .../src/actions/ai/chat/get-chats-action.ts | 43 - apps/app/src/actions/ai/chat/index.tsx | 181 ---- apps/app/src/actions/ai/chat/utils.tsx | 33 - .../src/actions/ai/clear-history-action.ts | 19 - .../ai/editor/generate-editor-content.ts | 50 -- apps/app/src/actions/ai/session.ts | 18 - apps/app/src/actions/ai/storage.ts | 165 ---- apps/app/src/actions/ai/types.ts | 56 -- apps/app/src/actions/schema.ts | 1 + .../(app)/(dashboard)/(home)/layout.tsx | 2 +- .../(app)/(dashboard)/(home)/page.tsx | 26 +- .../evidence-tasks/(overview)/layout.tsx | 29 - .../evidence-tasks/(overview)/page.tsx | 17 - .../frameworks/[frameworkId]/page.tsx | 27 +- .../(app)/(dashboard)/integrations/page.tsx | 31 +- .../(dashboard)/people/[employeeId]/page.tsx | 25 +- .../(app)/(dashboard)/people/page.tsx | 27 +- .../(dashboard)/policies/(overview)/page.tsx | 26 +- .../(app)/(dashboard)/policies/[id]/page.tsx | 22 +- .../(app)/(dashboard)/policies/all/layout.tsx | 2 +- .../(app)/(dashboard)/policies/all/page.tsx | 30 +- .../(dashboard)/risk/(overview)/page.tsx | 26 +- .../risk/[riskId]/comments/page.tsx | 17 + .../(app)/(dashboard)/risk/[riskId]/page.tsx | 18 +- .../risk/[riskId]/tasks/[taskId]/page.tsx | 17 + .../(app)/(dashboard)/risk/register/page.tsx | 26 +- .../(dashboard)/settings/members/page.tsx | 26 +- .../(app)/(dashboard)/settings/page.tsx | 27 +- apps/app/src/app/[locale]/layout.tsx | 4 +- apps/app/src/app/api/chat/route.ts | 1 - apps/app/src/components/browser/browser.tsx | 95 -- .../app/src/components/browser/chat-input.tsx | 82 -- .../src/components/browser/chat-message.tsx | 135 --- .../app/src/components/browser/chat-panel.tsx | 172 ---- apps/app/src/components/chat/chat-avatar.tsx | 45 - apps/app/src/components/chat/chat-empty.tsx | 17 - .../app/src/components/chat/chat-examples.tsx | 65 -- apps/app/src/components/chat/chat-footer.tsx | 23 - apps/app/src/components/chat/chat-list.tsx | 28 - apps/app/src/components/chat/examples.ts | 5 - apps/app/src/components/chat/index.tsx | 133 --- apps/app/src/components/chat/messages.tsx | 104 --- apps/app/src/components/chat/spinner.tsx | 16 - apps/app/src/components/file-uploader.tsx | 2 +- .../forms/create-organization-form.tsx | 23 +- .../frameworks/framework-overview.tsx | 1 - apps/app/src/components/header.tsx | 4 - apps/app/src/components/main-menu.tsx | 18 +- apps/app/src/components/sidebar.tsx | 2 +- .../tables/risk-register/columns.tsx | 2 +- .../risk-register/data-table-header.tsx | 2 +- apps/app/src/components/upload-dialog.tsx | 3 +- apps/app/src/locales/en.ts | 35 +- apps/web/package.json | 22 +- apps/web/src/app/layout.tsx | 2 + bun.lockb | Bin 669072 -> 676928 bytes package.json | 2 +- packages/db/package.json | 6 +- packages/db/prisma/seed.js | 89 +- packages/db/src/index.js | 1 - packages/db/src/index.ts | 1 - packages/ui/src/components/badge.tsx | 4 +- packages/ui/src/components/dialog.tsx | 6 +- yarn.lock | 842 +++++++++++------- 66 files changed, 1006 insertions(+), 2051 deletions(-) delete mode 100644 apps/app/src/actions/ai/assistant-settings-action.ts delete mode 100644 apps/app/src/actions/ai/chat/get-chats-action.ts delete mode 100644 apps/app/src/actions/ai/chat/index.tsx delete mode 100644 apps/app/src/actions/ai/chat/utils.tsx delete mode 100644 apps/app/src/actions/ai/clear-history-action.ts delete mode 100644 apps/app/src/actions/ai/editor/generate-editor-content.ts delete mode 100644 apps/app/src/actions/ai/session.ts delete mode 100644 apps/app/src/actions/ai/storage.ts delete mode 100644 apps/app/src/actions/ai/types.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/layout.tsx delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/page.tsx delete mode 100644 apps/app/src/components/browser/browser.tsx delete mode 100644 apps/app/src/components/browser/chat-input.tsx delete mode 100644 apps/app/src/components/browser/chat-message.tsx delete mode 100644 apps/app/src/components/browser/chat-panel.tsx delete mode 100644 apps/app/src/components/chat/chat-avatar.tsx delete mode 100644 apps/app/src/components/chat/chat-empty.tsx delete mode 100644 apps/app/src/components/chat/chat-examples.tsx delete mode 100644 apps/app/src/components/chat/chat-footer.tsx delete mode 100644 apps/app/src/components/chat/chat-list.tsx delete mode 100644 apps/app/src/components/chat/examples.ts delete mode 100644 apps/app/src/components/chat/index.tsx delete mode 100644 apps/app/src/components/chat/messages.tsx delete mode 100644 apps/app/src/components/chat/spinner.tsx diff --git a/apps/app/package.json b/apps/app/package.json index 3062aa1..3c22c36 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -10,20 +10,20 @@ "clean-react": "rm -rf node_modules/react; rm -rf node_modules/react-dom" }, "dependencies": { - "@ai-sdk/anthropic": "^1.1.6", - "@ai-sdk/google": "^1.1.8", - "@ai-sdk/openai": "^1.1.9", - "@browserbasehq/sdk": "^2.2.0", + "@ai-sdk/anthropic": "^1.1.7", + "@ai-sdk/google": "^1.1.12", + "@ai-sdk/openai": "^1.1.10", + "@browserbasehq/sdk": "^2.3.0", "@bubba/notifications": "workspace:*", "@date-fns/tz": "^1.2.0", "@hookform/resolvers": "^3.10.0", "@nangohq/frontend": "^0.48.4", "@nangohq/node": "^0.48.4", - "@novu/headless": "^2.0.4", + "@novu/headless": "^2.6.5", "@novu/react": "^2.6.5", - "@prisma/instrumentation": "^6.3.0", + "@prisma/instrumentation": "^6.3.1", "@tanstack/react-query": "^5.66.0", - "@tanstack/react-table": "^8.20.6", + "@tanstack/react-table": "^8.21.2", "@tiptap/extension-bullet-list": "^2.11.5", "@tiptap/extension-heading": "^2.11.5", "@tiptap/extension-list-item": "^2.11.5", @@ -38,32 +38,32 @@ "@trigger.dev/react-hooks": "3.3.13", "@trigger.dev/sdk": "3.3.13", "@types/xml2js": "^0.4.14", - "@uploadthing/react": "^7.1.5", + "@uploadthing/react": "^7.2.0", "@upstash/ratelimit": "^2.0.5", - "ai": "^4.1.16", + "ai": "^4.1.36", "argon2": "^0.41.1", "bun": "^1.2.2", "crypto": "^1.0.1", - "dub": "^0.46.21", + "dub": "^0.46.29", "framer-motion": "^11.18.2", "geist": "^1.3.1", "highlight.js": "^11.11.1", - "ky": "^1.7.4", - "languine": "^3.0.1", - "marked": "^15.0.6", - "next": "^15.1.6", + "ky": "^1.7.5", + "languine": "^3.0.4", + "marked": "^15.0.7", + "next": "^15.1.7", "next-auth": "^5.0.0-beta.25", "next-international": "^1.3.1", "next-intl": "^3.26.3", - "next-safe-action": "^7.10.2", + "next-safe-action": "^7.10.3", "next-themes": "^0.4.4", "novel": "^1.0.2", - "novu": "^2.2.2", + "novu": "^2.6.5", "nuqs": "^2.3.2", "playwright-core": "^1.50.1", - "posthog-js": "^1.215.3", - "posthog-node": "^4.4.1", - "puppeteer-core": "^24.1.1", + "posthog-js": "^1.217.2", + "posthog-node": "^4.5.2", + "puppeteer-core": "^24.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", @@ -72,14 +72,14 @@ "react-markdown": "^9.0.3", "react-textarea-autosize": "^8.5.7", "react-use-draggable-scroll": "^0.4.7", - "remark-gfm": "^4.0.0", + "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", - "resend": "^4.1.1", + "resend": "^4.1.2", "sonner": "^1.7.4", "stripe": "^17.6.0", "tiptap-markdown": "^0.8.10", "ts-pattern": "^5.6.2", - "uploadthing": "^7.4.4", + "uploadthing": "^7.5.0", "use-debounce": "^10.0.4", "use-long-press": "^3.2.0", "xml2js": "^0.6.2", @@ -89,10 +89,10 @@ "devDependencies": { "@bubba/db": "workspace:*", "@trigger.dev/build": "3.3.13", - "@types/node": "^22.13.0", + "@types/node": "^22.13.2", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", - "postcss": "^8.5.1", + "postcss": "^8.5.2", "tailwindcss": "^3.4.17", "typescript": "^5.7.3" } diff --git a/apps/app/src/actions/ai/assistant-settings-action.ts b/apps/app/src/actions/ai/assistant-settings-action.ts deleted file mode 100644 index 1ac2a13..0000000 --- a/apps/app/src/actions/ai/assistant-settings-action.ts +++ /dev/null @@ -1,30 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { authActionClient } from "../safe-action"; -import { assistantSettingsSchema } from "../schema"; -import { getAssistantSettings, setAssistantSettings } from "./storage"; - -export const assistantSettingsAction = authActionClient - .schema(assistantSettingsSchema) - .metadata({ - name: "assistant-settings", - }) - .action(async ({ parsedInput: params, ctx: { user } }) => { - const settings = await getAssistantSettings(); - - if (!user?.organizationId || !user?.id) { - throw new Error("User not found"); - } - - await setAssistantSettings({ - settings, - params, - userId: user.id, - organizationId: user.organizationId, - }); - - revalidatePath("/account/assistant"); - - return params; - }); diff --git a/apps/app/src/actions/ai/chat/get-chats-action.ts b/apps/app/src/actions/ai/chat/get-chats-action.ts deleted file mode 100644 index 28d1ff1..0000000 --- a/apps/app/src/actions/ai/chat/get-chats-action.ts +++ /dev/null @@ -1,43 +0,0 @@ -"use server"; - -import { addMonths, addWeeks, isAfter, isBefore, isToday } from "date-fns"; -import { getChats } from "../storage"; -import type { Chat } from "../types"; - -export async function getChatsAction() { - const data = await getChats(); - - if (!data.length) { - return []; - } - - const base: { "1d": Chat[]; "7d": Chat[]; "30d": Chat[] } = { - "1d": [], - "7d": [], - "30d": [], - }; - - for (const obj of data) { - const currentDate = new Date(obj.createdAt); - - if (isToday(currentDate)) { - base["1d"].push(obj); - } - - if ( - !isToday(currentDate) && - isBefore(currentDate, addWeeks(currentDate, 1)) - ) { - base["7d"].push(obj); - } - - if ( - isAfter(currentDate, addWeeks(currentDate, 1)) && - isBefore(currentDate, addMonths(currentDate, 1)) - ) { - base["30d"].push(obj); - } - } - - return base; -} diff --git a/apps/app/src/actions/ai/chat/index.tsx b/apps/app/src/actions/ai/chat/index.tsx deleted file mode 100644 index ae3c82d..0000000 --- a/apps/app/src/actions/ai/chat/index.tsx +++ /dev/null @@ -1,181 +0,0 @@ -"use server"; - -import { auth } from "@/auth"; -import { BotMessage, SpinnerMessage } from "@/components/chat/messages"; -import { openai } from "@ai-sdk/openai"; -import { client as RedisClient } from "@bubba/kv"; -import { Ratelimit } from "@upstash/ratelimit"; -import { - createAI, - createStreamableValue, - getMutableAIState, - streamUI, -} from "ai/rsc"; -import { startOfMonth, subMonths } from "date-fns"; -import { nanoid } from "nanoid"; -import { headers } from "next/headers"; -import { getAssistantSettings, saveChat } from "../storage"; -import type { AIState, Chat, ClientMessage, UIState } from "../types"; - -const ratelimit = new Ratelimit({ - limiter: Ratelimit.fixedWindow(10, "10s"), - redis: RedisClient, -}); - -export async function submitUserMessage( - content: string, -): Promise { - "use server"; - const headerList = await headers(); - - const ip = headerList.get("x-forwarded-for"); - const session = await auth(); - - const { success } = await ratelimit.limit(ip ?? ""); - - const aiState = getMutableAIState(); - - if (!success) { - aiState.update({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: "assistant", - content: - "Not so fast, tiger. You've reached your message limit. Please wait a minute and try again.", - }, - ], - }); - - return { - id: nanoid(), - role: "assistant", - display: ( - - ), - }; - } - - const user = session?.user; - const organizationId = user?.organizationId as string; - - const defaultValues = { - from: subMonths(startOfMonth(new Date()), 12).toISOString(), - to: new Date().toISOString(), - }; - - aiState.update({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: "user", - content, - }, - ], - }); - - let textStream: undefined | ReturnType>; - let textNode: undefined | React.ReactNode; - - const result = await streamUI({ - model: openai("gpt-4o-mini"), - initial: , - system: `\ - You are a helpful assistant in Midday who can help users ask questions about their transactions, revenue, spending find invoices and more. - - If the user wants the burn rate, call \`getBurnRate\` function. - If the user wants the runway, call \`getRunway\` function. - If the user wants the profit, call \`getProfit\` function. - If the user wants to find transactions or expenses, call \`getTransactions\` function. - If the user wants to see spending based on a category, call \`getSpending\` function. - If the user wants to find invoices or receipts, call \`getInvoices\` function. - If the user wants to find documents, call \`getDocuments\` function. - Don't return markdown, just plain text. - - Always try to call the functions with default values, otherwise ask the user to respond with parameters. - Current date is: ${new Date().toISOString().split("T")[0]} \n - `, - messages: [ - ...aiState.get().messages.map((message: any) => ({ - role: message.role, - content: message.content, - name: message.name, - display: null, - })), - ], - text: ({ content, done, delta }) => { - if (!textStream) { - textStream = createStreamableValue(""); - textNode = ; - } - - if (done) { - textStream.done(); - aiState.done({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: "assistant", - content, - }, - ], - }); - } else { - textStream.update(delta); - } - - return textNode; - }, - tools: {}, - }); - - return { - id: nanoid(), - role: "assistant", - display: result.value, - }; -} - -export async function handleAIStateChange(state: AIState, done: boolean) { - "use server"; - - const settings = await getAssistantSettings(); - - if (!done || !settings?.enabled) return; - - const createdAt = new Date(); - const userId = state.user.id; - const organizationId = state.user.organizationId; - - const { chatId, messages } = state; - - const firstMessageContent = messages?.at(0)?.content ?? ""; - const title = - typeof firstMessageContent === "string" - ? firstMessageContent.substring(0, 100) - : ""; - - const chat: Chat = { - id: chatId, - title, - userId, - createdAt, - messages, - organizationId, - }; - - await saveChat(chat); -} - -export const AI = createAI({ - actions: { - submitUserMessage, - }, - initialUIState: [], -}); diff --git a/apps/app/src/actions/ai/chat/utils.tsx b/apps/app/src/actions/ai/chat/utils.tsx deleted file mode 100644 index 9273dfa..0000000 --- a/apps/app/src/actions/ai/chat/utils.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { BotMessage, UserMessage } from "@/components/chat/messages"; -import type { Chat } from "../types"; - -export const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - -function getUIComponentFromMessage(message: any) { - if (message.role === "user") { - return {message.content}; - } - - if (message.role === "assistant" && typeof message.content === "string") { - return ; - } - - if (message.role === "tool") { - return message.content.map((tool: any) => { - switch (tool.toolName) { - default: - return null; - } - }); - } -} - -export const getUIStateFromAIState = (aiState: Chat) => { - return aiState?.messages - .filter((message) => message.role !== "system") - .map((message, index) => ({ - id: `${aiState.id}-${index}`, - display: getUIComponentFromMessage(message), - })); -}; diff --git a/apps/app/src/actions/ai/clear-history-action.ts b/apps/app/src/actions/ai/clear-history-action.ts deleted file mode 100644 index e1c8f33..0000000 --- a/apps/app/src/actions/ai/clear-history-action.ts +++ /dev/null @@ -1,19 +0,0 @@ -"use server"; - -import { authActionClient } from "../safe-action"; -import { clearChats } from "./storage"; - -export const clearHistoryAction = authActionClient - .metadata({ - name: "clear-history", - }) - .action(async ({ ctx: { user } }) => { - if (!user?.organizationId || !user?.id) { - throw new Error("User not found"); - } - - return clearChats({ - organizationId: user.organizationId, - userId: user.id, - }); - }); diff --git a/apps/app/src/actions/ai/editor/generate-editor-content.ts b/apps/app/src/actions/ai/editor/generate-editor-content.ts deleted file mode 100644 index 066abb0..0000000 --- a/apps/app/src/actions/ai/editor/generate-editor-content.ts +++ /dev/null @@ -1,50 +0,0 @@ -"use server"; - -import { openai } from "@ai-sdk/openai"; -import { streamText } from "ai"; -import { createStreamableValue } from "ai/rsc"; - -type Params = { - input: string; - context?: string; -}; - -export async function generateEditorContent({ input, context }: Params) { - const stream = createStreamableValue(""); - - (async () => { - const { textStream } = await streamText({ - model: openai("gpt-4o-mini"), - prompt: input, - temperature: 0.8, - system: ` - You are an expert AI assistant specializing in invoice-related content generation and improvement. Your task is to enhance or modify invoice text based on specific instructions. Follow these guidelines: - - 1. Language: Always respond in the same language as the input prompt. - 2. Conciseness: Keep responses brief and precise, with a maximum of 200 characters. - - You will perform one of these primary functions: - - Fix grammar: Rectify any grammatical errors while preserving the original meaning. - - Improve text: Refine the text to improve clarity and professionalism. - - Condense text: Remove any unnecessary text and only keep the invoice-related content and make it more concise. - - Format your response as plain text, using '\n' for line breaks when necessary. - Do not include any titles or headings in your response. - Provide only invoice-relevant content without any extraneous information. - Begin your response directly with the relevant invoice text or information. - - For custom prompts, maintain focus on invoice-related content. Ensure all generated text is appropriate for formal business communications and adheres to standard invoice practices. - Current date is: ${new Date().toISOString().split("T")[0]} \n - ${context} -`, - }); - - for await (const delta of textStream) { - stream.update(delta); - } - - stream.done(); - })(); - - return { output: stream.value }; -} diff --git a/apps/app/src/actions/ai/session.ts b/apps/app/src/actions/ai/session.ts deleted file mode 100644 index 250ed6e..0000000 --- a/apps/app/src/actions/ai/session.ts +++ /dev/null @@ -1,18 +0,0 @@ -"use server"; - -import { - closeBrowserSession, - createSession, - getSessionUrl, -} from "@/lib/operator/session"; - -export async function createAndGetSessionUrl() { - const session = await createSession(); - const url = await getSessionUrl(session.id); - - return { sessionId: session.id, url }; -} - -export async function closeSession(sessionId: string) { - await closeBrowserSession(sessionId); -} \ No newline at end of file diff --git a/apps/app/src/actions/ai/storage.ts b/apps/app/src/actions/ai/storage.ts deleted file mode 100644 index ac8b89c..0000000 --- a/apps/app/src/actions/ai/storage.ts +++ /dev/null @@ -1,165 +0,0 @@ -"use server"; - -import { auth } from "@/auth"; -import type { Chat, SettingsResponse } from "./types"; - -import { Redis } from "@upstash/redis"; - -const redis = new Redis({ - url: process.env.UPSTASH_REDIS_REST_URL!, - token: process.env.UPSTASH_REDIS_REST_TOKEN!, -}); - -export async function getAssistantSettings(): Promise { - const session = await auth(); - const user = session?.user; - - const organizationId = user?.organizationId; - const userId = user?.id; - - const defaultSettings: SettingsResponse = { - enabled: true, - }; - - const settings = await redis.get( - `assistant:${organizationId}:user:${userId}:settings`, - ); - - return { - ...defaultSettings, - ...(settings || {}), - }; -} - -type SetAassistant = { - settings: SettingsResponse; - userId: string; - organizationId: string; - params: { - enabled?: boolean | undefined; - }; -}; - -export async function setAssistantSettings({ - settings, - params, - userId, - organizationId, -}: SetAassistant) { - return redis.set(`assistant:${organizationId}:user:${userId}:settings`, { - ...settings, - ...params, - }); -} - -export async function clearChats({ - organizationId, - userId, -}: { organizationId: string; userId: string }) { - const chats: string[] = await redis.zrange( - `chat:${organizationId}:user:${userId}`, - 0, - -1, - ); - - const pipeline = redis.pipeline(); - - for (const chat of chats) { - pipeline.del(chat); - pipeline.zrem(`chat:${organizationId}:user:${userId}`, chat); - } - - await pipeline.exec(); -} - -export async function getLatestChat() { - const settings = await getAssistantSettings(); - if (!settings?.enabled) return null; - - const session = await auth(); - const user = session?.user; - - const organizationId = user?.organizationId; - const userId = user?.id; - - try { - const chat: string[] = await redis.zrange( - `chat:${organizationId}:user:${userId}`, - 0, - 1, - { - rev: true, - }, - ); - - const lastId = chat.at(0); - - if (lastId) { - return redis.hgetall(lastId); - } - } catch (error) { - return null; - } -} - -export async function getChats() { - const session = await auth(); - const user = session?.user; - - const organizationId = user?.organizationId; - const userId = user?.id; - - try { - const pipeline = redis.pipeline(); - const chats: string[] = await redis.zrange( - `chat:${organizationId}:user:${userId}`, - 0, - -1, - { - rev: true, - }, - ); - - for (const chat of chats) { - pipeline.hgetall(chat); - } - - const results = await pipeline.exec(); - - return results as Chat[]; - } catch (error) { - return []; - } -} - -export async function getChat(id: string) { - const session = await auth(); - const user = session?.user; - - const userId = user?.id; - - const chat = await redis.hgetall(`chat:${id}`); - - if (!chat || (userId && chat.userId !== userId)) { - return null; - } - - return chat; -} - -export async function saveChat(chat: Chat) { - const pipeline = redis.pipeline(); - pipeline.hmset(`chat:${chat.id}`, chat); - - const chatKey = `chat:${chat.organizationId}:user:${chat.userId}`; - - pipeline - .zadd(chatKey, { - score: Date.now(), - member: `chat:${chat.id}`, - }) - // Expire in 30 days - .expire(chatKey, 2592000); - - await pipeline.exec(); -} diff --git a/apps/app/src/actions/ai/types.ts b/apps/app/src/actions/ai/types.ts deleted file mode 100644 index d0bc90e..0000000 --- a/apps/app/src/actions/ai/types.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { CoreMessage } from "ai"; -import type { ReactNode } from "react"; - -export type Message = CoreMessage & { - id: string; -}; - -export interface Chat extends Record { - id: string; - title: string; - createdAt: Date; - userId: string; - organizationId: string; - messages: Message[]; -} - -export type SettingsResponse = { - enabled: boolean; -}; - -export type User = { - id: string; - organizationId: string; - full_name: string; - avatar_url: string; -}; - -export type AIState = { - chatId: string; - user: User; - messages: Message[]; -}; - -export type UIState = { - id: string; - display: React.ReactNode; -}[]; - -export interface ServerMessage { - role: "user" | "assistant" | "tool"; - content: string; -} - -export interface ClientMessage { - id: string; - role: "user" | "assistant"; - display: ReactNode; -} - -type ValueOrUpdater = T | ((prevState: T) => T); - -export type MutableAIState = { - get: () => AIState; - update: (newState: ValueOrUpdater) => void; - done: ((newState: AIState) => void) | (() => void); -}; diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index 424af45..784e84f 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -9,6 +9,7 @@ import { import { z } from "zod"; export const organizationSchema = z.object({ + fullName: z.string().min(1, "Full name is required"), name: z.string().min(1, "Name is required"), website: z.string().url("Must be a valid URL"), subdomain: z.string().min(1, "Subdomain is required").optional(), diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/layout.tsx index e3156ac..1c4bb2f 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/layout.tsx @@ -9,7 +9,7 @@ export default async function Layout({ const t = await getI18n(); return ( -
+
{children}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/page.tsx index fb8a786..af7767a 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/page.tsx @@ -1,5 +1,29 @@ +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { FrameworksOverview } from "./Components/FrameworksOverview"; -export default function DashboardPage() { +export default async function DashboardPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); + return ; } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.overview"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/layout.tsx deleted file mode 100644 index 513df54..0000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/layout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@bubba/ui/secondary-menu"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/page.tsx deleted file mode 100644 index 0028ee0..0000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence-tasks/(overview)/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { auth } from "@/auth"; -import { Browser } from "@/components/browser/browser"; -import { redirect } from "next/navigation"; - -export default async function RiskManagement() { - const session = await auth(); - - if (!session?.user?.organizationId) { - redirect("/onboarding"); - } - - return ( -
- -
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/page.tsx index f1e66f3..2d319dd 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/page.tsx @@ -2,7 +2,10 @@ import { auth } from "@/auth"; import { FrameworkControls } from "@/components/frameworks/framework-controls"; import { FrameworkOverview } from "@/components/frameworks/framework-overview"; import { SkeletonLoader } from "@/components/skeleton-loader"; +import { getI18n } from "@/locales/server"; import { db } from "@bubba/db"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { unstable_cache } from "next/cache"; import { redirect } from "next/navigation"; import { Suspense } from "react"; @@ -10,12 +13,14 @@ import { Suspense } from "react"; interface PageProps { params: Promise<{ frameworkId: string; + locale: string; }>; } export default async function FrameworkPage({ params }: PageProps) { const session = await auth(); - const { frameworkId } = await params; + const { frameworkId, locale } = await params; + setStaticParamsLocale(locale); if (!session?.user?.organizationId) { redirect("/login"); @@ -65,7 +70,7 @@ const getFramework = unstable_cache( ["framework-cache"], { tags: ["framework-cache"], - } + }, ); const getOrganizationFramework = unstable_cache( @@ -88,7 +93,7 @@ const getOrganizationFramework = unstable_cache( ["org-framework-cache"], { tags: ["org-framework-cache"], - } + }, ); const getFrameworkCategories = unstable_cache( @@ -130,5 +135,19 @@ const getFrameworkCategories = unstable_cache( ["framework-categories-cache"], { tags: ["framework-categories-cache"], - } + }, ); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string; frameworkId: string }>; +}): Promise { + const { locale, frameworkId } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: `${t("sub_pages.frameworks.overview")}`, + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/integrations/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/integrations/page.tsx index f2581c7..cbb34cb 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/integrations/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/integrations/page.tsx @@ -1,22 +1,23 @@ import { auth } from "@/auth"; -import { DeleteOrganization } from "@/components/forms/organization/delete-organization"; -import { UpdateOrganizationName } from "@/components/forms/organization/update-organization-name"; -import { UpdateOrganizationWebsite } from "@/components/forms/organization/update-organization-website"; import { IntegrationsHeader } from "@/components/integrations/integrations-header"; import { IntegrationsServer } from "@/components/integrations/integrations.server"; import { SkeletonLoader } from "@/components/skeleton-loader"; +import { getI18n } from "@/locales/server"; import { db } from "@bubba/db"; import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; import { Suspense } from "react"; -export const metadata: Metadata = { - title: "Integrations | Comp AI", -}; +export default async function IntegrationsPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); -export default async function OrganizationSettings() { const session = await auth(); - const [organization] = await Promise.all([ db.organization.findUnique({ where: { @@ -39,3 +40,17 @@ export default async function OrganizationSettings() {
); } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.integrations"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/page.tsx index 23d7faa..1b043cc 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/page.tsx @@ -1,12 +1,18 @@ import { auth } from "@/auth"; +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; import { EmployeeDetails } from "./components/EmployeeDetails"; export default async function EmployeeDetailsPage({ params, }: { - params: Promise<{ employeeId: string }>; + params: Promise<{ locale: string; employeeId: string }>; }) { + const { locale, employeeId } = await params; + setStaticParamsLocale(locale); + const session = await auth(); const organizationId = session?.user.organizationId; @@ -14,7 +20,20 @@ export default async function EmployeeDetailsPage({ redirect("/"); } - const { employeeId } = await params; - return ; } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string; employeeId: string }>; +}): Promise { + const { locale } = await params; + + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.people.employee_details"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/page.tsx index ab1f80f..f189e85 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/page.tsx @@ -1,9 +1,19 @@ import { auth } from "@/auth"; import { getServerColumnHeaders } from "@/components/tables/people/server-columns"; +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; import { EmployeesList } from "./components/EmployeesList"; -export default async function PeoplePage() { +export default async function PeoplePage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); + const session = await auth(); const organizationId = session?.user.organizationId; @@ -15,3 +25,18 @@ export default async function PeoplePage() { return ; } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.people"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/(overview)/page.tsx index 1c77f38..8c9a627 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/(overview)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/(overview)/page.tsx @@ -1,8 +1,18 @@ import { auth } from "@/auth"; +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; import { PoliciesOverview } from "./Components/PoliciesOverview"; -export default async function PoliciesOverviewPage() { +export default async function PoliciesOverviewPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); + const session = await auth(); if (!session?.user?.organizationId) { @@ -11,3 +21,17 @@ export default async function PoliciesOverviewPage() { return ; } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.policies"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx index b257dd6..0687651 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx @@ -1,11 +1,29 @@ import { PolicyOverview } from "@/components/policies/policy-overview"; +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; interface PageProps { - params: Promise<{ id: string }>; + params: Promise<{ locale: string; id: string }>; } export default async function PolicyPage({ params }: PageProps) { - const { id } = await params; + const { locale, id } = await params; + setStaticParamsLocale(locale); return ; } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string; id: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.policies.editor"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/all/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/all/layout.tsx index 2376071..bf11906 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/all/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/all/layout.tsx @@ -10,7 +10,7 @@ export default async function Layout({ const t = await getI18n(); return ( -
+
Loading...
}> ; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); -export default async function PoliciesPage() { const session = await auth(); if (!session?.user?.organizationId) { @@ -42,3 +52,17 @@ export default async function PoliciesPage() { ); } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string; id: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.policies.all"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/(overview)/page.tsx index a36fd68..931ea81 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/(overview)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/(overview)/page.tsx @@ -1,11 +1,21 @@ import { auth } from "@/auth"; import { RiskOverview } from "@/components/risks/charts/risk-overview"; import { RisksByAssignee } from "@/components/risks/charts/risks-by-assignee"; +import { getI18n } from "@/locales/server"; import { db } from "@bubba/db"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { unstable_cache } from "next/cache"; import { redirect } from "next/navigation"; -export default async function RiskManagement() { +export default async function RiskManagement({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); + const session = await auth(); if (!session?.user?.organizationId) { @@ -96,3 +106,17 @@ const getRiskOverview = unstable_cache( }, ["risk-overview-cache"], ); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.risk"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/comments/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/comments/page.tsx index 1c815a2..9db344e 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/comments/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/comments/page.tsx @@ -1,6 +1,9 @@ import { auth } from "@/auth"; import { RiskComments } from "@/components/risks/risk-comments"; +import { getI18n } from "@/locales/server"; import { db } from "@bubba/db"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { unstable_cache } from "next/cache"; import { redirect } from "next/navigation"; @@ -67,3 +70,17 @@ const getUsers = unstable_cache( }, ["users-cache"], ); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.risk.risk_comments"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/page.tsx index 009eb5b..09fd07d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/page.tsx @@ -14,6 +14,8 @@ import { getServerColumnHeaders } from "@/components/tables/risk-tasks/server-co import { getI18n } from "@/locales/server"; import { type RiskTaskStatus, db } from "@bubba/db"; import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { unstable_cache } from "next/cache"; import { redirect } from "next/navigation"; @@ -25,7 +27,7 @@ interface PageProps { page?: string; per_page?: string; }>; - params: Promise<{ riskId: string }>; + params: Promise<{ riskId: string; locale: string }>; } export default async function RiskPage({ searchParams, params }: PageProps) { @@ -232,3 +234,17 @@ const getUsers = unstable_cache( }, ["users-cache"], ); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.risk.risk_overview"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/page.tsx index 2e9b0fe..adda350 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/page.tsx @@ -4,7 +4,10 @@ import { TaskComment } from "@/components/risks/tasks/task-comments"; import { TaskOverview } from "@/components/risks/tasks/task-overview"; import { env } from "@/env.mjs"; +import { getI18n } from "@/locales/server"; import { db } from "@bubba/db"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { unstable_cache } from "next/cache"; import { redirect } from "next/navigation"; @@ -110,3 +113,17 @@ const getUsers = unstable_cache( }, ["users-cache"], ); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.risk.tasks.task_overview"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/register/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/register/page.tsx index b410831..4181aff 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/risk/register/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/risk/register/page.tsx @@ -11,11 +11,17 @@ import { import { FilterToolbar } from "@/components/tables/risk-register/filter-toolbar"; import { Loading } from "@/components/tables/risk-register/loading"; import { getServerColumnHeaders } from "@/components/tables/risk-register/server-columns"; +import { getI18n } from "@/locales/server"; import { type Departments, type RiskStatus, db } from "@bubba/db"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { unstable_cache } from "next/cache"; import { redirect } from "next/navigation"; -interface PageProps { +export default async function RiskRegisterPage({ + searchParams, +}: { + params: Promise<{ locale: string }>; searchParams: Promise<{ search?: string; category?: string; @@ -25,9 +31,7 @@ interface PageProps { page?: string; per_page?: string; }>; -} - -export default async function RiskRegisterPage({ searchParams }: PageProps) { +}) { const session = await auth(); const organizationId = session?.user.organizationId; const columnHeaders = await getServerColumnHeaders(); @@ -178,3 +182,17 @@ const getRisks = unstable_cache( }, ["risks-cache"], ); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.risk.register"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/page.tsx index f9dbba6..969b924 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/page.tsx @@ -1,9 +1,33 @@ import { TeamMembers } from "@/components/settings/team/team-members"; +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; + +export default async function Members({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); -export default async function Members() { return (
); } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sub_pages.settings.members"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx index bb82533..63836d9 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx @@ -2,15 +2,20 @@ import { auth } from "@/auth"; import { DeleteOrganization } from "@/components/forms/organization/delete-organization"; import { UpdateOrganizationName } from "@/components/forms/organization/update-organization-name"; import { UpdateOrganizationWebsite } from "@/components/forms/organization/update-organization-website"; +import { getI18n } from "@/locales/server"; import { db } from "@bubba/db"; import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; -export const metadata: Metadata = { - title: "Organization Settings | Comp AI", -}; +export default async function OrganizationSettings({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); -export default async function OrganizationSettings() { const session = await auth(); const [organization] = await Promise.all([ @@ -38,3 +43,17 @@ export default async function OrganizationSettings() {
); } + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.settings"), + }; +} diff --git a/apps/app/src/app/[locale]/layout.tsx b/apps/app/src/app/[locale]/layout.tsx index 04eb184..b67e927 100644 --- a/apps/app/src/app/[locale]/layout.tsx +++ b/apps/app/src/app/[locale]/layout.tsx @@ -67,8 +67,8 @@ export const viewport = { ], }; -export const preferredRegion = ["fra1", "sfo1", "iad1"]; -export const maxDuration = 60; +export const preferredRegion = ["auto"]; +export const maxDuration = 5; if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) { initializeServer({ diff --git a/apps/app/src/app/api/chat/route.ts b/apps/app/src/app/api/chat/route.ts index 0a90266..0100487 100644 --- a/apps/app/src/app/api/chat/route.ts +++ b/apps/app/src/app/api/chat/route.ts @@ -14,7 +14,6 @@ import { } from "ai"; import { z } from "zod"; import { - clickableElementsPrompt, getCachedMessages, } from "./lib/prompts"; import { diff --git a/apps/app/src/components/browser/browser.tsx b/apps/app/src/components/browser/browser.tsx deleted file mode 100644 index 41b4a3e..0000000 --- a/apps/app/src/components/browser/browser.tsx +++ /dev/null @@ -1,95 +0,0 @@ -"use client"; - -import { closeSession, createAndGetSessionUrl } from "@/actions/ai/session"; -import { ChatInput } from "@/components/browser/chat-input"; -import { ChatPanel } from "@/components/browser/chat-panel"; -import { Button } from "@bubba/ui/button"; -import React from "react"; -import { useInView } from "react-intersection-observer"; - -export function Browser() { - const initialInputRef = React.useRef(null); - const [sessionUrl, setSessionUrl] = React.useState(null); - const [sessionId, setSessionId] = React.useState(null); - const [isInitializing, setIsInitializing] = React.useState(false); - const [initialMessage, setInitialMessage] = React.useState( - null, - ); - const [isEnding, setIsEnding] = React.useState(false); - - const initializeSession = async () => { - if (sessionId || isInitializing) return; - setIsInitializing(true); - try { - const { url, sessionId: id } = await createAndGetSessionUrl(); - setSessionUrl(url); - setSessionId(id); - } catch (error) { - console.error("Failed to initialize session:", error); - } finally { - setIsInitializing(false); - } - }; - - const handleInitialSubmit = async (value: string) => { - setInitialMessage(value); - await initializeSession(); - }; - - const handleExampleClick = async (prompt: string) => { - setInitialMessage(prompt); - await initializeSession(); - }; - - const handleEndSession = async () => { - if (!sessionId) return; - setIsEnding(true); - try { - await closeSession(sessionId); - setSessionId(null); - setSessionUrl(null); - window.location.reload(); - } catch (error) { - console.error("Failed to end session:", error); - } finally { - setIsEnding(false); - } - }; - - return ( -
-
- {!initialMessage ? ( -
-
-

Evidence Agent

-
- -
- -
-
- ) : ( -
- -
- )} -
-
- ); -} diff --git a/apps/app/src/components/browser/chat-input.tsx b/apps/app/src/components/browser/chat-input.tsx deleted file mode 100644 index d1544cb..0000000 --- a/apps/app/src/components/browser/chat-input.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import { Button } from "@bubba/ui/button"; -import { cn } from "@bubba/ui/cn"; -import { Send } from "lucide-react"; -import * as React from "react"; -import TextareaAutosize, { - type TextareaAutosizeProps, -} from "react-textarea-autosize"; - -interface ChatInputProps - extends Omit { - onValueChange?: (value: string) => void; - onSubmit?: (value: string) => Promise | void; -} - -const ChatInput = React.forwardRef( - (props, forwardedRef) => { - const { - value, - onValueChange, - onSubmit: onSubmitProp, - disabled, - className, - ...chatInputProps - } = props; - - async function onKeyDown(event: React.KeyboardEvent) { - if (event.key === "Enter" && !event.shiftKey && !disabled) { - event.preventDefault(); - const textarea = event.currentTarget; - const value = textarea.value.trim(); - if (!value) return; - - if (onSubmitProp) { - await onSubmitProp(value); - if (onValueChange) onValueChange(""); - } - } - } - - async function onSubmit() { - if (disabled) return; - const textValue = typeof value === "string" ? value.trim() : ""; - if (!textValue) return; - - if (onSubmitProp) { - await onSubmitProp(textValue); - if (onValueChange) onValueChange(""); - } - } - - return ( -
- onValueChange?.(event.target.value)} - onKeyDown={onKeyDown} - disabled={disabled} - {...chatInputProps} - /> - -
- ); - }, -); - -export { ChatInput }; diff --git a/apps/app/src/components/browser/chat-message.tsx b/apps/app/src/components/browser/chat-message.tsx deleted file mode 100644 index d3d104d..0000000 --- a/apps/app/src/components/browser/chat-message.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client"; - -import { cn } from "@bubba/ui/cn"; -import type { Message } from "ai"; -import { - CheckCircle2, - Loader2, - Mouse, - Navigation, - ScrollText, - Search, -} from "lucide-react"; -import { motion } from "motion/react"; - -function getToolIcon(toolName: string) { - switch (toolName) { - case "searchGoogle": - return ; - case "navigate": - return ; - case "browserAction": - return ; - case "viewAllClickableElements": - return ; - default: - return null; - } -} - -interface ChatMessageProps { - message: Message & { - status?: { - type: string; - content: string; - step: number; - }; - }; -} - -function getStatusIcon(type: string) { - switch (type) { - case "status": - return ; - case "step": - return ; - default: - return null; - } -} - -export function ChatMessage({ message }: ChatMessageProps) { - return ( - -
-
- {message.content} -
- {/* {message.role === "assistant" && message.status && ( - - - {getStatusIcon(message.status.type)} - - {message.status.content === "initialized" - ? "Initializing..." - : message.status.content === "step_complete" - ? `Step ${message.status.step} completed` - : message.status.content === "finished" - ? "Finished" - : message.status.content} - - - - )} */} - {message.role === "assistant" && - message.toolInvocations && - message.toolInvocations.length > 0 && ( - - {message.toolInvocations.map((toolInvocation) => { - const { toolName, toolCallId, args, state } = toolInvocation; - - return ( - -
-
- {getToolIcon(toolName)} -
- - {toolName - .replace(/([A-Z])/g, " $1") - .replace(/^./, (str) => str.toUpperCase())} - - {state === "partial-call" && ( - - )} -
-
- ); - })} -
- )} -
-
- ); -} diff --git a/apps/app/src/components/browser/chat-panel.tsx b/apps/app/src/components/browser/chat-panel.tsx deleted file mode 100644 index 0495cb5..0000000 --- a/apps/app/src/components/browser/chat-panel.tsx +++ /dev/null @@ -1,172 +0,0 @@ -"use client"; - -import { ChatInput } from "@/components/browser/chat-input"; -import { ChatMessage } from "@/components/browser/chat-message"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { ResizablePanel, ResizablePanelGroup } from "@bubba/ui/resizable"; -import { useChat } from "ai/react"; -import React from "react"; -import { useInView } from "react-intersection-observer"; - -interface ChatPanelProps { - sessionId: string | null; - sessionUrl: string | null; - initialMessage: string | null; - onEndSession: () => void; - isEnding: boolean; - isInitializing: boolean; -} - -export function ChatPanel({ - sessionId, - sessionUrl, - initialMessage, - onEndSession, - isEnding, - isInitializing, -}: ChatPanelProps) { - const messagesEndRef = React.useRef(null); - const chatInputRef = React.useRef(null); - const [shouldAutoScroll, setShouldAutoScroll] = React.useState(true); - - const { messages, input, setInput, handleSubmit, isLoading, data, append } = - useChat({ - body: { sessionId }, - id: sessionId || undefined, - }); - - const [inViewRef, inView] = useInView({ threshold: 0 }); - - const composedScrollRef = React.useCallback( - (node: HTMLDivElement | null) => { - messagesEndRef.current = node; - inViewRef(node); - }, - [inViewRef], - ); - - const isMobile = useIsMobile(); - - // Merge `data` into the last assistant message as a status, if any - const messagesWithStatus = React.useMemo(() => { - if (!data || !messages) return messages; - - const lastData = data[data.length - 1]; - if (!lastData) return messages; - - const lastMessage = messages[messages.length - 1]; - if (!lastMessage || lastMessage.role !== "assistant") return messages; - - return messages.map((message, index) => { - if (index === messages.length - 1 && message.role === "assistant") { - return { ...message, status: lastData }; - } - return message; - }); - }, [messages, data]); - - // Send initial message when component mounts - React.useEffect(() => { - if (initialMessage && sessionId) { - append({ - content: initialMessage, - role: "user", - }); - } - }, [sessionId, initialMessage, append]); - - // Scroll to bottom when new messages arrive - const scrollToBottom = React.useCallback(() => { - if (shouldAutoScroll) { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - } - }, [shouldAutoScroll]); - - React.useEffect(() => { - scrollToBottom(); - }, [messages, scrollToBottom]); - - const handleScroll = (e: React.UIEvent) => { - const target = e.target as HTMLDivElement; - const isAtBottom = - Math.abs(target.scrollHeight - target.scrollTop - target.clientHeight) < - 50; - setShouldAutoScroll(isAtBottom); - }; - - return ( - - -
-
- {isInitializing ? ( -
- Initializing browser session... -
- ) : ( - <> - {messagesWithStatus?.map((message) => ( - - ))} -
- - )} -
- -
- handleSubmit(new Event("submit"))} - disabled={isLoading || isInitializing || !sessionId} - /> -
-
- - - -
-
- {isInitializing ? ( -
-

Loading browser...

-
- ) : !sessionUrl ? ( -
-

Initializing

-
- ) : ( -