From ba50f0652baa2b917018911d70474f866bdfc84a Mon Sep 17 00:00:00 2001 From: czhen <56986964+shczhen@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:34:53 +0800 Subject: [PATCH 1/4] apply new ui --- playground/.gitignore | 5 +- playground/.yarnrc.yml | 1 + playground/Dockerfile | 60 +- playground/components.json | 21 + playground/next-env.d.ts | 2 +- playground/package.json | 45 +- playground/postcss.config.js | 6 +- playground/src/app/global.css | 112 +- playground/src/app/index.module.scss | 93 - playground/src/app/layout.tsx | 29 +- playground/src/app/test/page.tsx | 49 + playground/src/common/constant.ts | 109 +- playground/src/common/mock.ts | 31 +- .../src/components/Agent/AudioVisualizer.tsx | 52 + playground/src/components/Agent/Camera.tsx | 95 + .../src/components/Agent/Microphone.tsx | 179 + .../src/components/Agent/StreamPlayer.tsx | 58 + playground/src/components/Agent/View.tsx | 39 + .../components/Agent/VoicePresetSelect.tsx | 47 + .../src/components/Button/LoadingButton.tsx | 17 + playground/src/components/Chat/ChatCard.tsx | 215 + .../src/components/Chat/ChatCfgSelect.tsx | 286 + .../src/components/Chat/MessageList.tsx | 56 + playground/src/components/Chat/PdfSelect.tsx | 191 + playground/src/components/Dialog/Settings.tsx | 116 + .../components/Dynamic/NetworkIndicator.tsx | 29 + playground/src/components/Dynamic/RTCCard.tsx | 144 + playground/src/components/Icon/index.tsx | 532 + playground/src/components/Layout/Action.tsx | 165 + .../src/components/Layout/Header.module.css | 18 + playground/src/components/Layout/Header.tsx | 26 + .../components/Layout/HeaderComponents.tsx | 205 + playground/src/components/ui/avatar.tsx | 50 + playground/src/components/ui/button.tsx | 57 + playground/src/components/ui/card.tsx | 76 + playground/src/components/ui/checkbox.tsx | 30 + playground/src/components/ui/dialog.tsx | 122 + playground/src/components/ui/form.tsx | 178 + playground/src/components/ui/input.tsx | 25 + playground/src/components/ui/label.tsx | 26 + playground/src/components/ui/popover.tsx | 33 + playground/src/components/ui/select.tsx | 159 + playground/src/components/ui/sheet.tsx | 140 + playground/src/components/ui/sonner.tsx | 31 + playground/src/components/ui/switch.tsx | 29 + playground/src/components/ui/tabs.tsx | 55 + playground/src/components/ui/textarea.tsx | 24 + playground/src/components/ui/tooltip.tsx | 32 + playground/src/lib/utils.ts | 22 + playground/src/manager/index.ts | 3 +- playground/src/manager/rtc/rtc.ts | 139 +- playground/src/manager/rtm/index.ts | 147 + playground/src/store/reducers/global.ts | 204 +- playground/src/types/index.ts | 87 +- playground/tailwind.config.js | 64 + playground/tsconfig.json | 3 +- playground/yarn.lock | 9425 +++++++++++++++++ 57 files changed, 13807 insertions(+), 387 deletions(-) create mode 100644 playground/.yarnrc.yml create mode 100644 playground/components.json delete mode 100644 playground/src/app/index.module.scss create mode 100644 playground/src/app/test/page.tsx create mode 100644 playground/src/components/Agent/AudioVisualizer.tsx create mode 100644 playground/src/components/Agent/Camera.tsx create mode 100644 playground/src/components/Agent/Microphone.tsx create mode 100644 playground/src/components/Agent/StreamPlayer.tsx create mode 100644 playground/src/components/Agent/View.tsx create mode 100644 playground/src/components/Agent/VoicePresetSelect.tsx create mode 100644 playground/src/components/Button/LoadingButton.tsx create mode 100644 playground/src/components/Chat/ChatCard.tsx create mode 100644 playground/src/components/Chat/ChatCfgSelect.tsx create mode 100644 playground/src/components/Chat/MessageList.tsx create mode 100644 playground/src/components/Chat/PdfSelect.tsx create mode 100644 playground/src/components/Dialog/Settings.tsx create mode 100644 playground/src/components/Dynamic/NetworkIndicator.tsx create mode 100644 playground/src/components/Dynamic/RTCCard.tsx create mode 100644 playground/src/components/Icon/index.tsx create mode 100644 playground/src/components/Layout/Action.tsx create mode 100644 playground/src/components/Layout/Header.module.css create mode 100644 playground/src/components/Layout/Header.tsx create mode 100644 playground/src/components/Layout/HeaderComponents.tsx create mode 100644 playground/src/components/ui/avatar.tsx create mode 100644 playground/src/components/ui/button.tsx create mode 100644 playground/src/components/ui/card.tsx create mode 100644 playground/src/components/ui/checkbox.tsx create mode 100644 playground/src/components/ui/dialog.tsx create mode 100644 playground/src/components/ui/form.tsx create mode 100644 playground/src/components/ui/input.tsx create mode 100644 playground/src/components/ui/label.tsx create mode 100644 playground/src/components/ui/popover.tsx create mode 100644 playground/src/components/ui/select.tsx create mode 100644 playground/src/components/ui/sheet.tsx create mode 100644 playground/src/components/ui/sonner.tsx create mode 100644 playground/src/components/ui/switch.tsx create mode 100644 playground/src/components/ui/tabs.tsx create mode 100644 playground/src/components/ui/textarea.tsx create mode 100644 playground/src/components/ui/tooltip.tsx create mode 100644 playground/src/lib/utils.ts create mode 100644 playground/src/manager/rtm/index.ts create mode 100644 playground/tailwind.config.js create mode 100644 playground/yarn.lock diff --git a/playground/.gitignore b/playground/.gitignore index 3bcf2bf5..fd787b63 100644 --- a/playground/.gitignore +++ b/playground/.gitignore @@ -129,7 +129,4 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* - -# lock -package-lock.json -yarn.lock +.yarn \ No newline at end of file diff --git a/playground/.yarnrc.yml b/playground/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/playground/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/playground/Dockerfile b/playground/Dockerfile index 63379d47..5136bbea 100644 --- a/playground/Dockerfile +++ b/playground/Dockerfile @@ -1,27 +1,69 @@ FROM node:20-alpine AS base -FROM base AS builder +# 1. Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat WORKDIR /app -# COPY .env.example .env +# Install dependencies based on the preferred package manager +COPY package.json .yarnrc* yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then corepack enable yarn && yarn --immutable; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# 2. Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN npm i --verbose && \ - npm run build +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +ENV NEXT_TELEMETRY_DISABLED=1 +RUN \ + if [ -f yarn.lock ]; then corepack enable yarn && yarn run build; \ + elif [ -f package-lock.json ]; then npm run build; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + else echo "Lockfile not found." && exit 1; \ + fi -FROM base AS runner +# 3. Production image, copy all the files and run next +FROM base AS runner WORKDIR /app -ENV NODE_ENV production +# Uncomment the following line in case you want to disable telemetry during runtime. +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache RUN mkdir .next +RUN chown nextjs:nodejs .next -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs EXPOSE 3000 -CMD HOSTNAME="0.0.0.0" node server.js \ No newline at end of file +ENV PORT=3000 + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD HOSTNAME="0.0.0.0" node server.js diff --git a/playground/components.json b/playground/components.json new file mode 100644 index 00000000..265bf78d --- /dev/null +++ b/playground/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/playground/next-env.d.ts b/playground/next-env.d.ts index 4f11a03d..40c3d680 100644 --- a/playground/next-env.d.ts +++ b/playground/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/playground/package.json b/playground/package.json index 1a8d6f4f..c3c06fe6 100644 --- a/playground/package.json +++ b/playground/package.json @@ -1,9 +1,12 @@ { - "name": "TEN Agent", - "version": "0.4.0", + "name": "ten_agent_playground", + "version": "0.5.0", "private": true, + "engines": { + "node": ">=20" + }, "scripts": { - "dev": "next dev", + "dev": "next dev --turbopack", "build": "next build", "start": "next start", "lint": "next lint", @@ -11,17 +14,38 @@ }, "dependencies": { "@ant-design/icons": "^5.3.7", + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-tooltip": "^1.1.3", "@reduxjs/toolkit": "^2.2.3", "agora-rtc-sdk-ng": "^4.21.0", + "agora-rtm": "^2.2.0", "antd": "^5.15.3", "axios": "^1.7.7", - "next": "14.2.4", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.454.0", + "next": "^15.0.3", + "next-themes": "^0.4.3", "protobufjs": "^7.2.5", "react": "^18", "react-colorful": "^5.6.1", "react-dom": "^18", + "react-hook-form": "^7.53.1", "react-redux": "^9.1.0", - "redux": "^5.0.1" + "redux": "^5.0.1", + "sonner": "^1.7.0", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "devDependencies": { "@minko-fe/postcss-pxtoviewport": "^1.3.2", @@ -30,13 +54,16 @@ "@types/react": "^18", "@types/react-dom": "^18", "@types/react-redux": "^7.1.22", - "autoprefixer": "^10.4.16", + "autoprefixer": "^10.4.20", "eslint": "^8", - "eslint-config-next": "14.2.4", - "postcss": "^8.4.31", + "eslint-config-next": "^15.0.3", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.8", "protobufjs-cli": "^1.1.2", "sass": "^1.77.5", + "tailwindcss": "^3.4.14", "typescript": "^5" }, - "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" + "packageManager": "yarn@4.5.1" } diff --git a/playground/postcss.config.js b/playground/postcss.config.js index ea748d4d..c96f6e62 100644 --- a/playground/postcss.config.js +++ b/playground/postcss.config.js @@ -1,10 +1,12 @@ +/** @type {import('postcss-load-config').Config} */ module.exports = { plugins: { + tailwindcss: {}, autoprefixer: {}, "@minko-fe/postcss-pxtoviewport": { viewportWidth: 375, exclude: /node_modules/, include: /\/src\/platform\/mobile\//, - } + }, }, -} +}; diff --git a/playground/src/app/global.css b/playground/src/app/global.css index 7a1e1861..fcee2be8 100644 --- a/playground/src/app/global.css +++ b/playground/src/app/global.css @@ -1,3 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + * { box-sizing: border-box; padding: 0; @@ -6,7 +10,7 @@ html, body { - background-color: #0F0F11; + background-color: #0f0f11; font-family: "PingFang SC"; height: 100%; } @@ -22,46 +26,92 @@ a { } } -.ant-select-arrow { - color: #667085 !important; -} - - -.ant-select-selection-item { - color: #667085 !important; -} - -.ant-select-selector { - border: 1px solid #272A2F !important; - background-color: #272A2F !important; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } } -.ant-select-dropdown { - background-color: #1E2024 !important; +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } -.ant-select-item { - background: #1E2024 !important; - color: var(--Grey-600, #667085) !important; +/* Custom Scrollbar Styles */ +::-webkit-scrollbar { + width: 10px; } -.ant-select-item-option-selected { - background: #272A2F !important; - color: var(--Grey-300, #EAECF0) !important; +::-webkit-scrollbar-track { + background: transparent; + border-radius: 5px; } - -.ant-popover-inner { - /* width: 260px !important; */ - background: #1E2025 !important; +::-webkit-scrollbar-thumb { + background: hsl(var(--muted)); + border-radius: 5px; } - -.ant-select-selection-placeholder { - color: var(--Grey-600, #667085) !important; +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground)); } - -.ant-empty-description { - color: var(--Grey-600, #667085) !important; +/* For Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: hsl(var(--muted)) transparent; } diff --git a/playground/src/app/index.module.scss b/playground/src/app/index.module.scss deleted file mode 100644 index 78585596..00000000 --- a/playground/src/app/index.module.scss +++ /dev/null @@ -1,93 +0,0 @@ -@function multiple-box-shadow($n) { - $value: '#{random(2000)}px #{random(2000)}px #FFF'; - - @for $i from 2 through $n { - $value: '#{$value}, #{random(2000)}px #{random(2000)}px #FFF'; - } - - @return unquote($value); -} - -$shadows-small: multiple-box-shadow(700); -$shadows-medium: multiple-box-shadow(200); -$shadows-big: multiple-box-shadow(100); - - -.login { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: hidden; - // background: radial-gradient(ellipse at bottom, #1B2735 0%, #090A0F 100%); - background: url("../assets/background.jpg") no-repeat center center; - background-size: cover; - box-sizing: border-box; - - .starts { - width: 1px; - height: 1px; - background: transparent; - box-shadow: $shadows-small; - animation: animStar 50s linear infinite; - - &:after { - content: " "; - position: absolute; - top: 2000px; - width: 1px; - height: 1px; - background: transparent; - box-shadow: $shadows-small - } - } - - .starts2 { - width: 2px; - height: 2px; - box-shadow: $shadows-medium; - animation: animStar 100s linear infinite; - - &:after { - content: " "; - position: absolute; - top: 2000px; - width: 2px; - height: 2px; - background: transparent; - box-shadow: $shadows-medium; - } - } - - .starts3 { - width: 3px; - height: 3px; - background: transparent; - box-shadow: $shadows-big; - animation: animStar 150s linear infinite; - - &:after { - content: " "; - position: absolute; - top: 2000px; - width: 3px; - height: 3px; - background: transparent; - box-shadow: $shadows-big; - } - - } - -} - - -@keyframes animStar { - from { - transform: translateY(0px) - } - - to { - transform: translateY(-2000px) - } -} diff --git a/playground/src/app/layout.tsx b/playground/src/app/layout.tsx index d23bb3d9..f5370ef2 100644 --- a/playground/src/app/layout.tsx +++ b/playground/src/app/layout.tsx @@ -1,17 +1,18 @@ -import { ConfigProvider } from "antd" +// import { ConfigProvider } from "antd"; import { StoreProvider } from "@/store"; import type { Metadata, Viewport } from "next"; +import { Toaster } from "@/components/ui/sonner"; -import './global.css' - +import "./global.css"; export const metadata: Metadata = { title: "TEN Agent | Real-Time Multimodal AI Agent", - description: "TEN Agent is an open-source multimodal AI agent that can speak, see, and access a knowledge base(RAG).", + description: + "TEN Agent is an open-source multimodal AI agent that can speak, see, and access a knowledge base(RAG).", appleWebApp: { capable: true, statusBarStyle: "black", - } + }, }; export const viewport: Viewport = { @@ -21,10 +22,7 @@ export const viewport: Viewport = { maximumScale: 1, userScalable: false, viewportFit: "cover", -} - - - +}; export default function RootLayout({ children, @@ -33,8 +31,8 @@ export default function RootLayout({ }>) { return ( - - + {/* - - {children} - - + > */} + {children} + {/* */} + ); diff --git a/playground/src/app/test/page.tsx b/playground/src/app/test/page.tsx new file mode 100644 index 00000000..9147bdcd --- /dev/null +++ b/playground/src/app/test/page.tsx @@ -0,0 +1,49 @@ +"use client"; + +import dynamic from "next/dynamic"; + +import AuthInitializer from "@/components/authInitializer"; +import { useAppSelector, EMobileActiveTab } from "@/common"; +import Header from "@/components/Layout/Header"; +import Action from "@/components/Layout/Action"; +import { cn } from "@/lib/utils"; + +const DynamicRTCCard = dynamic(() => import("@/components/Dynamic/RTCCard"), { + ssr: false, +}); +const DynamicChatCard = dynamic(() => import("@/components/Chat/ChatCard"), { + ssr: false, +}); + +export default function Home() { + const mobileActiveTab = useAppSelector( + (state) => state.global.mobileActiveTab + ); + + return ( + +
+
+ +
+ + +
+
+
+ ); +} diff --git a/playground/src/common/constant.ts b/playground/src/common/constant.ts index 97eb0a90..75231009 100644 --- a/playground/src/common/constant.ts +++ b/playground/src/common/constant.ts @@ -1,80 +1,95 @@ -import { IOptions, ColorItem, LanguageOptionItem, VoiceOptionItem, GraphOptionItem } from "@/types" -export const GITHUB_URL = "https://github.com/TEN-framework/TEN-Agent" -export const OPTIONS_KEY = "__options__" -export const OVERRIDEN_PROPERTIES_KEY = "__overriden__" +import { + IOptions, + ColorItem, + LanguageOptionItem, + VoiceOptionItem, + GraphOptionItem, +} from "@/types"; +export const GITHUB_URL = "https://github.com/TEN-framework/TEN-Agent"; +export const OPTIONS_KEY = "__options__"; +export const OVERRIDEN_PROPERTIES_KEY = "__overriden__"; export const DEFAULT_OPTIONS: IOptions = { channel: "", userName: "", - userId: 0 -} -export const DESCRIPTION = "TEN Agent is an open-source multimodal AI agent that can speak, see, and access a knowledge base(RAG)." + userId: 0, + appId: "", + token: "", +}; +export const DESCRIPTION = + "TEN Agent is an open-source multimodal AI agent that can speak, see, and access a knowledge base(RAG)."; export const LANGUAGE_OPTIONS: LanguageOptionItem[] = [ { label: "English", - value: "en-US" + value: "en-US", }, { label: "Chinese", - value: "zh-CN" + value: "zh-CN", }, { label: "Korean", - value: "ko-KR" + value: "ko-KR", }, { label: "Japanese", - value: "ja-JP" - } -] + value: "ja-JP", + }, +]; export const GRAPH_OPTIONS: GraphOptionItem[] = [ { label: "Voice Agent - OpenAI LLM + Azure TTS", - value: "va_openai_azure" + value: "va_openai_azure", }, { label: "Voice Agent with Vision - OpenAI LLM + Azure TTS", - value: "camera_va_openai_azure" + value: "camera_va_openai_azure", }, //{ // label: "Voice Agent with Knowledge - RAG + Qwen LLM + Cosy TTS", // value: "va_qwen_rag" // }, -] +]; export const isRagGraph = (graphName: string) => { - return graphName === "va_qwen_rag" -} + return graphName === "va_qwen_rag"; +}; export const VOICE_OPTIONS: VoiceOptionItem[] = [ { label: "Male", - value: "male" + value: "male", }, { label: "Female", - value: "female" - } -] -export const COLOR_LIST: ColorItem[] = [{ - active: "#0888FF", - default: "#143354" -}, { - active: "#563FD8", - default: "#2C2553" -}, -{ - active: "#18A957", - default: "#173526" -}, { - active: "#FFAB08", - default: "#423115" -}, { - active: "#FD5C63", - default: "#462629" -}, { - active: "#E225B2", - default: "#481C3F" -}] + value: "female", + }, +]; +export const COLOR_LIST: ColorItem[] = [ + { + active: "#0888FF", + default: "#143354", + }, + { + active: "#563FD8", + default: "#2C2553", + }, + { + active: "#18A957", + default: "#173526", + }, + { + active: "#FFAB08", + default: "#423115", + }, + { + active: "#FD5C63", + default: "#462629", + }, + { + active: "#E225B2", + default: "#481C3F", + }, +]; export type VoiceTypeMap = { [voiceType: string]: string; @@ -87,3 +102,13 @@ export type VendorNameMap = { export type LanguageMap = { [language: string]: VendorNameMap; }; + +export enum EMobileActiveTab { + AGENT = "agent", + CHAT = "chat", +} + +export const MOBILE_ACTIVE_TAB_MAP = { + [EMobileActiveTab.AGENT]: "Agent", + [EMobileActiveTab.CHAT]: "Chat", +}; diff --git a/playground/src/common/mock.ts b/playground/src/common/mock.ts index db1e2ff8..63752efb 100644 --- a/playground/src/common/mock.ts +++ b/playground/src/common/mock.ts @@ -1,6 +1,5 @@ -import { getRandomUserId } from "./utils" -import { IChatItem } from "@/types" - +import { getRandomUserId } from "./utils"; +import { IChatItem, EMessageType } from "@/types"; const SENTENCES = [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", @@ -10,32 +9,30 @@ const SENTENCES = [ "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", -] - +]; export const genRandomParagraph = (num: number = 0): string => { - let paragraph = "" + let paragraph = ""; for (let i = 0; i < num; i++) { - const randomIndex = Math.floor(Math.random() * SENTENCES.length) - paragraph += SENTENCES[randomIndex] + " " + const randomIndex = Math.floor(Math.random() * SENTENCES.length); + paragraph += SENTENCES[randomIndex] + " "; } - return paragraph.trim() -} - + return paragraph.trim(); +}; export const genRandomChatList = (num: number = 10): IChatItem[] => { - const arr: IChatItem[] = [] + const arr: IChatItem[] = []; for (let i = 0; i < num; i++) { - const type = Math.random() > 0.5 ? "agent" : "user" + const type = Math.random() > 0.5 ? EMessageType.AGENT : EMessageType.USER; arr.push({ userId: getRandomUserId(), - userName: type == "agent" ? "Agent" : "You", + userName: type == "agent" ? EMessageType.AGENT : "You", text: genRandomParagraph(3), type, time: Date.now(), - }) + }); } - return arr -} + return arr; +}; diff --git a/playground/src/components/Agent/AudioVisualizer.tsx b/playground/src/components/Agent/AudioVisualizer.tsx new file mode 100644 index 00000000..b4ca850c --- /dev/null +++ b/playground/src/components/Agent/AudioVisualizer.tsx @@ -0,0 +1,52 @@ +export interface AudioVisualizerProps { + type: "agent" | "user" + frequencies: Float32Array[] + gap: number + barWidth: number + minBarHeight: number + maxBarHeight: number + borderRadius: number +} + +export default function AudioVisualizer(props: AudioVisualizerProps) { + const { + frequencies, + gap, + barWidth, + minBarHeight, + maxBarHeight, + borderRadius, + type, + } = props + + const summedFrequencies = frequencies.map((bandFrequencies) => { + const sum = bandFrequencies.reduce((a, b) => a + b, 0) + if (sum <= 0) { + return 0 + } + return Math.sqrt(sum / bandFrequencies.length) + }) + + return ( +
+ {summedFrequencies.map((frequency, index) => { + const style = { + height: + minBarHeight + frequency * (maxBarHeight - minBarHeight) + "px", + borderRadius: borderRadius + "px", + width: barWidth + "px", + transition: + "background-color 0.35s ease-out, transform 0.25s ease-out", + // transform: transform, + backgroundColor: type === "agent" ? "#0888FF" : "#EAECF0", + boxShadow: type === "agent" ? "0 0 10px #EAECF0" : "none", + } + + return + })} +
+ ) +} diff --git a/playground/src/components/Agent/Camera.tsx b/playground/src/components/Agent/Camera.tsx new file mode 100644 index 00000000..68ead80b --- /dev/null +++ b/playground/src/components/Agent/Camera.tsx @@ -0,0 +1,95 @@ +"use client" + +import * as React from "react" +// import CamSelect from "./camSelect" +import { CamIconByStatus } from "@/components/Icon" +import AgoraRTC, { ICameraVideoTrack } from "agora-rtc-sdk-ng" +// import { LocalStreamPlayer } from "../streamPlayer" +// import { useSmallScreen } from "@/common" +import { + CommonDeviceWrapper, + TDeviceSelectItem, + DEFAULT_DEVICE_ITEM, + DeviceSelect, +} from "@/components/Agent/Microphone" +import { LocalStreamPlayer } from "@/components/Agent/StreamPlayer" + +export default function CameraBlock(props: { videoTrack?: ICameraVideoTrack }) { + const { videoTrack } = props + const [videoMute, setVideoMute] = React.useState(false) + + React.useEffect(() => { + videoTrack?.setMuted(videoMute) + }, [videoTrack, videoMute]) + + const onClickMute = () => { + setVideoMute(!videoMute) + } + + return ( + } + > +
+ +
+
+ ) +} + +interface SelectItem { + label: string + value: string + deviceId: string +} + +const DEFAULT_ITEM: SelectItem = { + label: "Default", + value: "default", + deviceId: "", +} + +const CamSelect = (props: { videoTrack?: ICameraVideoTrack }) => { + const { videoTrack } = props + const [items, setItems] = React.useState([DEFAULT_ITEM]) + const [value, setValue] = React.useState("default") + + React.useEffect(() => { + if (videoTrack) { + const label = videoTrack?.getTrackLabel() + setValue(label) + AgoraRTC.getCameras().then((arr) => { + setItems( + arr.map((item) => ({ + label: item.label, + value: item.label, + deviceId: item.deviceId, + })), + ) + }) + } + }, [videoTrack]) + + const onChange = async (value: string) => { + const target = items.find((item) => item.value === value) + if (target) { + setValue(target.value) + if (videoTrack) { + await videoTrack.setDevice(target.deviceId) + } + } + } + + return ( + + ) +} diff --git a/playground/src/components/Agent/Microphone.tsx b/playground/src/components/Agent/Microphone.tsx new file mode 100644 index 00000000..d162a4b5 --- /dev/null +++ b/playground/src/components/Agent/Microphone.tsx @@ -0,0 +1,179 @@ +"use client" + +import * as React from "react" +import { useMultibandTrackVolume } from "@/common" +import AudioVisualizer from "@/components/Agent/AudioVisualizer" +import AgoraRTC, { IMicrophoneAudioTrack } from "agora-rtc-sdk-ng" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Button } from "@/components/ui/button" +import { MicIconByStatus } from "@/components/Icon" + +export default function MicrophoneBlock(props: { + audioTrack?: IMicrophoneAudioTrack +}) { + const { audioTrack } = props + const [audioMute, setAudioMute] = React.useState(false) + const [mediaStreamTrack, setMediaStreamTrack] = + React.useState() + + React.useEffect(() => { + audioTrack?.on("track-updated", onAudioTrackupdated) + if (audioTrack) { + setMediaStreamTrack(audioTrack.getMediaStreamTrack()) + } + + return () => { + audioTrack?.off("track-updated", onAudioTrackupdated) + } + }, [audioTrack]) + + React.useEffect(() => { + audioTrack?.setMuted(audioMute) + }, [audioTrack, audioMute]) + + const subscribedVolumes = useMultibandTrackVolume(mediaStreamTrack, 20) + + const onAudioTrackupdated = (track: MediaStreamTrack) => { + console.log("[test] audio track updated", track) + setMediaStreamTrack(track) + } + + const onClickMute = () => { + setAudioMute(!audioMute) + } + + return ( + } + > +
+ +
+
+ ) +} + +export function CommonDeviceWrapper(props: { + children: React.ReactNode + title: string + Icon: ( + props: React.SVGProps & { active?: boolean }, + ) => React.ReactNode + onIconClick: () => void + isActive: boolean + select?: React.ReactNode +}) { + const { title, Icon, onIconClick, isActive, select, children } = props + + return ( +
+
+
{title}
+
+ + {select} +
+
+ {children} +
+ ) +} + +export type TDeviceSelectItem = { + label: string + value: string + deviceId: string +} + +export const DEFAULT_DEVICE_ITEM: TDeviceSelectItem = { + label: "Default", + value: "default", + deviceId: "", +} + +export const DeviceSelect = (props: { + items: TDeviceSelectItem[] + value: string + onChange: (value: string) => void + placeholder?: string +}) => { + const { items, value, onChange, placeholder } = props + + return ( + + ) +} + +export const MicrophoneSelect = (props: { + audioTrack?: IMicrophoneAudioTrack +}) => { + const { audioTrack } = props + const [items, setItems] = React.useState([ + DEFAULT_DEVICE_ITEM, + ]) + const [value, setValue] = React.useState("default") + + React.useEffect(() => { + if (audioTrack) { + const label = audioTrack?.getTrackLabel() + setValue(label) + AgoraRTC.getMicrophones().then((arr) => { + setItems( + arr.map((item) => ({ + label: item.label, + value: item.label, + deviceId: item.deviceId, + })), + ) + }) + } + }, [audioTrack]) + + const onChange = async (value: string) => { + const target = items.find((item) => item.value === value) + if (target) { + setValue(target.value) + if (audioTrack) { + await audioTrack.setDevice(target.deviceId) + } + } + } + + return +} diff --git a/playground/src/components/Agent/StreamPlayer.tsx b/playground/src/components/Agent/StreamPlayer.tsx new file mode 100644 index 00000000..1d322967 --- /dev/null +++ b/playground/src/components/Agent/StreamPlayer.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import { + ICameraVideoTrack, + IMicrophoneAudioTrack, + VideoPlayerConfig, +} from "agora-rtc-sdk-ng" + +export interface StreamPlayerProps { + videoTrack?: ICameraVideoTrack + audioTrack?: IMicrophoneAudioTrack + style?: React.CSSProperties + fit?: "cover" | "contain" | "fill" + onClick?: () => void + mute?: boolean +} + +export const LocalStreamPlayer = React.forwardRef( + (props: StreamPlayerProps, ref) => { + const { + videoTrack, + audioTrack, + mute = false, + style = {}, + fit = "cover", + onClick = () => {}, + } = props + const vidDiv = React.useRef(null) + + React.useLayoutEffect(() => { + const config = { fit } as VideoPlayerConfig + if (mute) { + videoTrack?.stop() + } else { + if (!videoTrack?.isPlaying) { + videoTrack?.play(vidDiv.current!, config) + } + } + + return () => { + videoTrack?.stop() + } + }, [videoTrack, fit, mute]) + + // local audio track need not to be played + // useLayoutEffect(() => {}, [audioTrack, localAudioMute]) + + return ( +
+ ) + }, +) diff --git a/playground/src/components/Agent/View.tsx b/playground/src/components/Agent/View.tsx new file mode 100644 index 00000000..c9b53df7 --- /dev/null +++ b/playground/src/components/Agent/View.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useMultibandTrackVolume } from "@/common" +import { cn } from "@/lib/utils" +// import AudioVisualizer from "../audioVisualizer" +import { IMicrophoneAudioTrack } from "agora-rtc-sdk-ng" +import AudioVisualizer from "@/components/Agent/AudioVisualizer" + +export interface AgentViewProps { + audioTrack?: IMicrophoneAudioTrack +} + +export default function AgentView(props: AgentViewProps) { + const { audioTrack } = props + + const subscribedVolumes = useMultibandTrackVolume(audioTrack, 12) + + return ( +
+
Agent
+
+ +
+
+ ) +} diff --git a/playground/src/components/Agent/VoicePresetSelect.tsx b/playground/src/components/Agent/VoicePresetSelect.tsx new file mode 100644 index 00000000..9cc90f8a --- /dev/null +++ b/playground/src/components/Agent/VoicePresetSelect.tsx @@ -0,0 +1,47 @@ +"use client" + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useAppSelector, useAppDispatch, VOICE_OPTIONS } from "@/common" +import { setVoiceType } from "@/store/reducers/global" +import type { VoiceType } from "@/types" +import { VoiceIcon } from "@/components/Icon" + +export default function AgentVoicePresetSelect() { + const dispatch = useAppDispatch() + const options = useAppSelector((state) => state.global.options) + const voiceType = useAppSelector((state) => state.global.voiceType) + + const onVoiceChange = (value: string) => { + dispatch(setVoiceType(value as VoiceType)) + } + + return ( + + ) +} diff --git a/playground/src/components/Button/LoadingButton.tsx b/playground/src/components/Button/LoadingButton.tsx new file mode 100644 index 00000000..533734fa --- /dev/null +++ b/playground/src/components/Button/LoadingButton.tsx @@ -0,0 +1,17 @@ +import { Button, ButtonProps } from "@/components/ui/button" +import { AnimatedSpinnerIcon } from "@/components/Icon" + +export interface LoadingButtonProps extends Omit { + loading?: boolean + svgProps?: React.SVGProps +} + +export function LoadingButton(props: LoadingButtonProps) { + const { loading, disabled, children, svgProps, ...rest } = props + return ( + + ) +} diff --git a/playground/src/components/Chat/ChatCard.tsx b/playground/src/components/Chat/ChatCard.tsx new file mode 100644 index 00000000..517b4b17 --- /dev/null +++ b/playground/src/components/Chat/ChatCard.tsx @@ -0,0 +1,215 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { + RemoteGraphSelect, + RemoteGraphCfgSheet, +} from "@/components/Chat/ChatCfgSelect"; +import PdfSelect from "@/components/Chat/PdfSelect"; +import { + genRandomChatList, + useAppDispatch, + useAutoScroll, + LANGUAGE_OPTIONS, + useAppSelector, + GRAPH_OPTIONS, + isRagGraph, + apiGetGraphs, + apiGetNodes, + useGraphExtensions, + apiGetExtensionMetadata, + apiReloadGraph, +} from "@/common"; +import { + setRtmConnected, + addChatItem, + setExtensionMetadata, + setGraphName, + setGraphs, + setLanguage, + setExtensions, + setOverridenPropertiesByGraph, + setOverridenProperties, +} from "@/store/reducers/global"; +import MessageList from "@/components/Chat/MessageList"; +import { Button } from "@/components/ui/button"; +import { Send } from "lucide-react"; +import { rtmManager } from "@/manager/rtm"; +import { type IRTMTextItem, EMessageType, ERTMTextType } from "@/types"; + +export default function ChatCard(props: { className?: string }) { + const { className } = props; + const [modal2Open, setModal2Open] = React.useState(false); + const [inputValue, setInputValue] = React.useState(""); + + const rtmConnected = useAppSelector((state) => state.global.rtmConnected); + const dispatch = useAppDispatch(); + const graphs = useAppSelector((state) => state.global.graphs); + const extensions = useAppSelector((state) => state.global.extensions); + const graphName = useAppSelector((state) => state.global.graphName); + const chatItems = useAppSelector((state) => state.global.chatItems); + const agentConnected = useAppSelector((state) => state.global.agentConnected); + const graphExtensions = useGraphExtensions(); + const extensionMetadata = useAppSelector( + (state) => state.global.extensionMetadata + ); + const overridenProperties = useAppSelector( + (state) => state.global.overridenProperties + ); + const options = useAppSelector((state) => state.global.options); + + const disableInputMemo = React.useMemo(() => { + return ( + !options.channel || + !options.userId || + !options.appId || + !options.token || + !rtmConnected || + !agentConnected + ); + }, [ + options.channel, + options.userId, + options.appId, + options.token, + rtmConnected, + agentConnected, + ]); + + // const chatItems = genRandomChatList(10) + const chatRef = React.useRef(null); + + React.useEffect(() => { + apiReloadGraph().then(() => { + Promise.all([apiGetGraphs(), apiGetExtensionMetadata()]).then( + (res: any) => { + let [graphRes, metadataRes] = res; + let graphs = graphRes["data"].map((item: any) => item["name"]); + + let metadata = metadataRes["data"]; + let metadataMap: Record = {}; + metadata.forEach((item: any) => { + metadataMap[item["name"]] = item; + }); + dispatch(setGraphs(graphs)); + dispatch(setExtensionMetadata(metadataMap)); + } + ); + }); + }, []); + + React.useEffect(() => { + if (!extensions[graphName]) { + apiGetNodes(graphName).then((res: any) => { + let nodes = res["data"]; + let nodesMap: Record = {}; + nodes.forEach((item: any) => { + nodesMap[item["name"]] = item; + }); + dispatch(setExtensions({ graphName, nodesMap })); + }); + } + }, [graphName]); + + useAutoScroll(chatRef); + + const onGraphNameChange = (val: any) => { + dispatch(setGraphName(val)); + }; + + const onTextChanged = (text: IRTMTextItem) => { + console.log("[rtm] onTextChanged", text); + if (text.type == ERTMTextType.TRANSCRIBE) { + // const isAgent = Number(text.uid) != Number(options.userId) + dispatch( + addChatItem({ + userId: options.userId, + text: text.text, + type: text.stream_id === "0" ? EMessageType.AGENT : EMessageType.USER, + isFinal: text.is_final, + time: text.ts, + }) + ); + } + if (text.type == ERTMTextType.INPUT_TEXT) { + dispatch( + addChatItem({ + userId: options.userId, + text: text.text, + type: EMessageType.USER, + isFinal: true, + time: text.ts, + }) + ); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleInputSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!inputValue || disableInputMemo) { + return; + } + rtmManager.sendText(inputValue); + setInputValue(""); + }; + + return ( + <> + {/* Chat Card */} +
+
+ {/* Action Bar */} +
+ + + {isRagGraph(graphName) && } +
+ {/* Chat messages would go here */} + +
+
+ + +
+
+
+
+ + ); +} diff --git a/playground/src/components/Chat/ChatCfgSelect.tsx b/playground/src/components/Chat/ChatCfgSelect.tsx new file mode 100644 index 00000000..6da1ca45 --- /dev/null +++ b/playground/src/components/Chat/ChatCfgSelect.tsx @@ -0,0 +1,286 @@ +"use client"; + +import * as React from "react"; +import { buttonVariants } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, + SheetFooter, + SheetClose, +} from "@/components/ui/sheet"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + useAppDispatch, + LANGUAGE_OPTIONS, + useAppSelector, + GRAPH_OPTIONS, + useGraphExtensions, +} from "@/common"; +import type { Language } from "@/types"; +import { + setGraphName, + setLanguage, + setOverridenPropertiesByGraph, +} from "@/store/reducers/global"; +import { cn } from "@/lib/utils"; +import { SettingsIcon } from "lucide-react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +export function RemoteGraphSelect() { + const dispatch = useAppDispatch(); + const graphName = useAppSelector((state) => state.global.graphName); + const graphs = useAppSelector((state) => state.global.graphs); + const agentConnected = useAppSelector((state) => state.global.agentConnected); + + const onGraphNameChange = (val: string) => { + dispatch(setGraphName(val)); + }; + + const graphOptions = graphs.map((item) => ({ + label: item, + value: item, + })); + + return ( + <> + + + ); +} + +export function RemoteGraphCfgSheet() { + const dispatch = useAppDispatch(); + const graphExtensions = useGraphExtensions(); + const graphName = useAppSelector((state) => state.global.graphName); + const extensionMetadata = useAppSelector( + (state) => state.global.extensionMetadata + ); + const overridenProperties = useAppSelector( + (state) => state.global.overridenProperties + ); + + const [selectedExtension, setSelectedExtension] = React.useState(""); + + return ( + + + + + + + Properties Override + + You can adjust extension properties here, the values will be + overridden when the agent starts using "Connect." Note that this + won't modify the property.json file. + + + +
+ + +
+ + {graphExtensions?.[selectedExtension]?.["property"] && ( + { + // clone the overridenProperties + let nodesMap = JSON.parse( + JSON.stringify(overridenProperties[selectedExtension] || {}) + ); + // Update initial data with any existing overridden values + if (overridenProperties[selectedExtension]) { + Object.assign(nodesMap, overridenProperties[selectedExtension]); + } + nodesMap[selectedExtension] = data; + console.log("nodesMap", nodesMap); + dispatch( + setOverridenPropertiesByGraph({ + graphName: selectedExtension, + nodesMap, + }) + ); + }} + /> + )} + + {/* + + + + */} +
+
+ ); +} + +// Helper to convert values based on type +const convertToType = (value: any, type: string) => { + switch (type) { + case "int64": + case "int32": + return parseInt(value, 10); + case "float64": + return parseFloat(value); + case "bool": + return value === true || value === "true"; + case "string": + return String(value); + default: + return value; + } +}; + +const GraphCfgForm = ({ + initialData, + metadata, + onUpdate, +}: { + initialData: Record; + metadata: Record; + onUpdate: (data: Record) => void; +}) => { + const formSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.null()]) + ); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: initialData, + }); + + const onSubmit = (data: z.infer) => { + const convertedData = Object.entries(data).reduce( + (acc, [key, value]) => { + const type = metadata[key]?.type || "string"; + acc[key] = value === "" ? null : convertToType(value, type); + return acc; + }, + {} as Record + ); + onUpdate(convertedData); + }; + + const initialDataWithType = Object.entries(initialData).reduce( + (acc, [key, value]) => { + acc[key] = { value, type: metadata[key]?.type || "string" }; + return acc; + }, + {} as Record< + string, + { value: string | number | boolean | null; type: string } + > + ); + + return ( +
+ + {Object.entries(initialDataWithType).map(([key, { value, type }]) => ( + ( + + {key} + + {type === "bool" ? ( +
+ +
+ ) : ( + + )} +
+
+ )} + /> + ))} + + + + ); +}; diff --git a/playground/src/components/Chat/MessageList.tsx b/playground/src/components/Chat/MessageList.tsx new file mode 100644 index 00000000..c75f0150 --- /dev/null +++ b/playground/src/components/Chat/MessageList.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { + useAppDispatch, + useAutoScroll, + LANGUAGE_OPTIONS, + useAppSelector, + GRAPH_OPTIONS, + isRagGraph, +} from "@/common" +import { EMessageType, type IChatItem } from "@/types" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { cn } from "@/lib/utils" + +export default function MessageList(props: { className?: string }) { + const { className } = props + + const chatItems = useAppSelector((state) => state.global.chatItems) + + const containerRef = React.useRef(null) + + useAutoScroll(containerRef) + + return ( +
+ {chatItems.map((item, index) => { + return + })} +
+ ) +} + +export function MessageItem(props: { data: IChatItem }) { + const { data } = props + + return ( + <> +
+ + + {data.type === EMessageType.AGENT ? "AG" : "U"} + + +
+

{data.text}

+
+
+ + ) +} diff --git a/playground/src/components/Chat/PdfSelect.tsx b/playground/src/components/Chat/PdfSelect.tsx new file mode 100644 index 00000000..c9959351 --- /dev/null +++ b/playground/src/components/Chat/PdfSelect.tsx @@ -0,0 +1,191 @@ +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { FileTextIcon } from "lucide-react" + +import { OptionType, IPdfData } from "@/types" +import { + apiGetDocumentList, + apiUpdateDocument, + useAppSelector, + genUUID, +} from "@/common" +import { toast } from "sonner" + +export default function PdfSelect() { + const options = useAppSelector((state) => state.global.options) + const { channel, userId } = options + const [pdfOptions, setPdfOptions] = React.useState([]) + const [selectedPdf, setSelectedPdf] = React.useState("") + const agentConnected = useAppSelector((state) => state.global.agentConnected) + + React.useEffect(() => { + if (agentConnected) { + getPDFOptions() + } + }, [agentConnected]) + + const getPDFOptions = async () => { + const res = await apiGetDocumentList() + setPdfOptions( + res.data.map((item: any) => { + return { + value: item.collection, + label: item.file_name, + } + }), + ) + setSelectedPdf("") + } + + const onUploadSuccess = (data: IPdfData) => { + setPdfOptions([ + ...pdfOptions, + { + value: data.collection, + label: data.fileName, + }, + ]) + setSelectedPdf(data.collection) + } + + const onSelectPdf = async (val: string) => { + const item = pdfOptions.find((item) => item.value === val) + if (!item) { + // return message.error("Please select a PDF file") + return + } + setSelectedPdf(val) + await apiUpdateDocument({ + collection: val, + fileName: item.label, + channel, + }) + } + + return ( + <> + + + + + + + Upload & Select PDF + + +
+ +
+
+
+ + ) +} + +export function UploadPdf({ + onSuccess, +}: { + onSuccess?: (data: IPdfData) => void +}) { + const agentConnected = useAppSelector((state) => state.global.agentConnected) + const options = useAppSelector((state) => state.global.options) + const { channel, userId } = options + const [uploading, setUploading] = React.useState(false) + + const handleUpload = async (e: React.ChangeEvent) => { + if (!agentConnected) { + toast.error("Please connect to agent first") + return + } + + const file = e.target.files?.[0] + if (!file) return + + setUploading(true) + + const formData = new FormData() + formData.append("file", file) + formData.append("channel_name", channel) + formData.append("uid", String(userId)) + formData.append("request_id", genUUID()) + + try { + const response = await fetch("/api/vector/document/upload", { + method: "POST", + body: formData, + }) + const data = await response.json() + + if (data.code === "0") { + toast.success(`Upload ${file.name} success`) + const { collection, file_name } = data.data + onSuccess?.({ + fileName: file_name, + collection, + }) + } else { + toast.info(data.msg) + } + } catch (err) { + toast.error(`Upload ${file.name} failed`) + } finally { + setUploading(false) + } + } + + return ( +
+ +
+ ) +} diff --git a/playground/src/components/Dialog/Settings.tsx b/playground/src/components/Dialog/Settings.tsx new file mode 100644 index 00000000..78c02249 --- /dev/null +++ b/playground/src/components/Dialog/Settings.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Textarea } from "@/components/ui/textarea" +import { SettingsIcon } from "lucide-react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { useAppDispatch, useAppSelector } from "@/common" +import { setAgentSettings } from "@/store/reducers/global" + +const formSchema = z.object({ + greeting: z.string(), + prompt: z.string(), +}) + +export default function SettingsDialog() { + const [open, setOpen] = React.useState(false) + + const dispatch = useAppDispatch() + const agentSettings = useAppSelector((state) => state.global.agentSettings) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + greeting: agentSettings.greeting, + prompt: agentSettings.prompt, + }, + }) + + function onSubmit(values: z.infer) { + console.log("Form Values:", values) + dispatch(setAgentSettings(values)) + setOpen(false) + } + + return ( + + + + + + + Settings + + +
+ + ( + + Greeting + +