Spaces:
Running
Running
Upload 122 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- OpenAIChatAssistant/.config/configstore/firebase-tools.json +15 -0
- OpenAIChatAssistant/.config/configstore/update-notifier-firebase-tools.json +4 -0
- OpenAIChatAssistant/.config/gh/config.yml +17 -0
- OpenAIChatAssistant/.config/gh/hosts.yml +7 -0
- OpenAIChatAssistant/.github/workflows/static.yml +43 -0
- OpenAIChatAssistant/.gitignore +38 -0
- OpenAIChatAssistant/.replit +45 -0
- OpenAIChatAssistant/OpenChat/LICENSE +21 -0
- OpenAIChatAssistant/OpenChatAI/.gitattributes +35 -0
- OpenAIChatAssistant/OpenChatAI/README.md +10 -0
- OpenAIChatAssistant/OpenChatAI/index.html +19 -0
- OpenAIChatAssistant/OpenChatAI/style.css +28 -0
- OpenAIChatAssistant/README.md +1 -0
- OpenAIChatAssistant/attached_assets/content-1746193714894.md +7 -0
- OpenAIChatAssistant/attached_assets/content-1746193784985.md +1467 -0
- OpenAIChatAssistant/attached_assets/content-1746448603342.md +23 -0
- OpenAIChatAssistant/attached_assets/screenshot-1746193704446.png +0 -0
- OpenAIChatAssistant/attached_assets/screenshot-1746193776733.png +0 -0
- OpenAIChatAssistant/client/index.html +13 -0
- OpenAIChatAssistant/client/src/App.tsx +43 -0
- OpenAIChatAssistant/client/src/components/ChatHistory.tsx +126 -0
- OpenAIChatAssistant/client/src/components/ChatInputForm.tsx +72 -0
- OpenAIChatAssistant/client/src/components/ConnectionStatus.tsx +64 -0
- OpenAIChatAssistant/client/src/components/ConversationSidebar.tsx +315 -0
- OpenAIChatAssistant/client/src/components/ImageGenerator.tsx +319 -0
- OpenAIChatAssistant/client/src/components/TypingIndicator.tsx +30 -0
- OpenAIChatAssistant/client/src/components/UserSettingsModal.tsx +348 -0
- OpenAIChatAssistant/client/src/components/VideoGenerator.tsx +169 -0
- OpenAIChatAssistant/client/src/components/ui/accordion.tsx +56 -0
- OpenAIChatAssistant/client/src/components/ui/alert-dialog.tsx +139 -0
- OpenAIChatAssistant/client/src/components/ui/alert.tsx +59 -0
- OpenAIChatAssistant/client/src/components/ui/aspect-ratio.tsx +5 -0
- OpenAIChatAssistant/client/src/components/ui/avatar.tsx +50 -0
- OpenAIChatAssistant/client/src/components/ui/badge.tsx +36 -0
- OpenAIChatAssistant/client/src/components/ui/breadcrumb.tsx +115 -0
- OpenAIChatAssistant/client/src/components/ui/button.tsx +56 -0
- OpenAIChatAssistant/client/src/components/ui/calendar.tsx +68 -0
- OpenAIChatAssistant/client/src/components/ui/card.tsx +79 -0
- OpenAIChatAssistant/client/src/components/ui/carousel.tsx +260 -0
- OpenAIChatAssistant/client/src/components/ui/chart.tsx +365 -0
- OpenAIChatAssistant/client/src/components/ui/checkbox.tsx +28 -0
- OpenAIChatAssistant/client/src/components/ui/collapsible.tsx +11 -0
- OpenAIChatAssistant/client/src/components/ui/command.tsx +151 -0
- OpenAIChatAssistant/client/src/components/ui/context-menu.tsx +198 -0
- OpenAIChatAssistant/client/src/components/ui/dialog.tsx +120 -0
- OpenAIChatAssistant/client/src/components/ui/drawer.tsx +118 -0
- OpenAIChatAssistant/client/src/components/ui/dropdown-menu.tsx +198 -0
- OpenAIChatAssistant/client/src/components/ui/form.tsx +176 -0
- OpenAIChatAssistant/client/src/components/ui/hover-card.tsx +29 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
OpenAIChatAssistant/generated-icon.png filter=lfs diff=lfs merge=lfs -text
|
OpenAIChatAssistant/.config/configstore/firebase-tools.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"motd": {
|
| 3 |
+
"cloudBuildErrorAfter": 1594252800000,
|
| 4 |
+
"cloudBuildWarnAfter": 1590019200000,
|
| 5 |
+
"defaultNode10After": 1594252800000,
|
| 6 |
+
"minVersion": "3.0.5",
|
| 7 |
+
"node8DeploysDisabledAfter": 1613390400000,
|
| 8 |
+
"node8RuntimeDisabledAfter": 1615809600000,
|
| 9 |
+
"node8WarnAfter": 1600128000000,
|
| 10 |
+
"fetched": 1746200259916
|
| 11 |
+
},
|
| 12 |
+
"usage": true,
|
| 13 |
+
"analytics-uuid": "39420299-5030-4806-bca2-b74705ec958b",
|
| 14 |
+
"lastError": 1746202219061
|
| 15 |
+
}
|
OpenAIChatAssistant/.config/configstore/update-notifier-firebase-tools.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"optOut": false,
|
| 3 |
+
"lastUpdateCheck": 1746200259769
|
| 4 |
+
}
|
OpenAIChatAssistant/.config/gh/config.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The current version of the config schema
|
| 2 |
+
version: 1
|
| 3 |
+
# What protocol to use when performing git operations. Supported values: ssh, https
|
| 4 |
+
git_protocol: https
|
| 5 |
+
# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.
|
| 6 |
+
editor:
|
| 7 |
+
# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
|
| 8 |
+
prompt: enabled
|
| 9 |
+
# A pager program to send command output to, e.g. "less". If blank, will refer to environment. Set the value to "cat" to disable the pager.
|
| 10 |
+
pager:
|
| 11 |
+
# Aliases allow you to create nicknames for gh commands
|
| 12 |
+
aliases:
|
| 13 |
+
co: pr checkout
|
| 14 |
+
# The path to a unix socket through which send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport.
|
| 15 |
+
http_unix_socket:
|
| 16 |
+
# What web browser gh should use when opening URLs. If blank, will refer to environment.
|
| 17 |
+
browser:
|
OpenAIChatAssistant/.config/gh/hosts.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com:
|
| 2 |
+
users:
|
| 3 |
+
Bella288:
|
| 4 |
+
oauth_token: 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
|
| 5 |
+
git_protocol: https
|
| 6 |
+
oauth_token: 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
|
| 7 |
+
user: Bella288
|
OpenAIChatAssistant/.github/workflows/static.yml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simple workflow for deploying static content to GitHub Pages
|
| 2 |
+
name: Deploy static content to Pages
|
| 3 |
+
|
| 4 |
+
on:
|
| 5 |
+
# Runs on pushes targeting the default branch
|
| 6 |
+
push:
|
| 7 |
+
branches: ["main"]
|
| 8 |
+
|
| 9 |
+
# Allows you to run this workflow manually from the Actions tab
|
| 10 |
+
workflow_dispatch:
|
| 11 |
+
|
| 12 |
+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
| 13 |
+
permissions:
|
| 14 |
+
contents: read
|
| 15 |
+
pages: write
|
| 16 |
+
id-token: write
|
| 17 |
+
|
| 18 |
+
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
| 19 |
+
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
| 20 |
+
concurrency:
|
| 21 |
+
group: "pages"
|
| 22 |
+
cancel-in-progress: false
|
| 23 |
+
|
| 24 |
+
jobs:
|
| 25 |
+
# Single deploy job since we're just deploying
|
| 26 |
+
deploy:
|
| 27 |
+
environment:
|
| 28 |
+
name: github-pages
|
| 29 |
+
url: ${{ steps.deployment.outputs.page_url }}
|
| 30 |
+
runs-on: ubuntu-latest
|
| 31 |
+
steps:
|
| 32 |
+
- name: Checkout
|
| 33 |
+
uses: actions/checkout@v4
|
| 34 |
+
- name: Setup Pages
|
| 35 |
+
uses: actions/configure-pages@v5
|
| 36 |
+
- name: Upload artifact
|
| 37 |
+
uses: actions/upload-pages-artifact@v3
|
| 38 |
+
with:
|
| 39 |
+
# Upload entire repository
|
| 40 |
+
path: '.'
|
| 41 |
+
- name: Deploy to GitHub Pages
|
| 42 |
+
id: deployment
|
| 43 |
+
uses: actions/deploy-pages@v4
|
OpenAIChatAssistant/.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# Dependencies
|
| 3 |
+
node_modules
|
| 4 |
+
.pnp
|
| 5 |
+
.pnp.js
|
| 6 |
+
|
| 7 |
+
# Production
|
| 8 |
+
dist
|
| 9 |
+
build
|
| 10 |
+
|
| 11 |
+
# Testing
|
| 12 |
+
coverage
|
| 13 |
+
|
| 14 |
+
# Environment
|
| 15 |
+
.env
|
| 16 |
+
.env.local
|
| 17 |
+
.env.development.local
|
| 18 |
+
.env.test.local
|
| 19 |
+
.env.production.local
|
| 20 |
+
|
| 21 |
+
# Logs
|
| 22 |
+
npm-debug.log*
|
| 23 |
+
yarn-debug.log*
|
| 24 |
+
yarn-error.log*
|
| 25 |
+
|
| 26 |
+
# Editor/IDE
|
| 27 |
+
.vscode
|
| 28 |
+
.idea
|
| 29 |
+
*.swp
|
| 30 |
+
*.swo
|
| 31 |
+
|
| 32 |
+
# OS
|
| 33 |
+
.DS_Store
|
| 34 |
+
Thumbs.db
|
| 35 |
+
|
| 36 |
+
# Replit specific
|
| 37 |
+
.replit
|
| 38 |
+
.config
|
OpenAIChatAssistant/.replit
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
modules = ["nodejs-20", "web", "postgresql-16", "python-3.11"]
|
| 2 |
+
run = "npm run dev"
|
| 3 |
+
hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
|
| 4 |
+
|
| 5 |
+
[nix]
|
| 6 |
+
channel = "stable-24_05"
|
| 7 |
+
packages = ["gh"]
|
| 8 |
+
|
| 9 |
+
[deployment]
|
| 10 |
+
deploymentTarget = "autoscale"
|
| 11 |
+
build = ["sh", "-c", "npm run build"]
|
| 12 |
+
run = ["sh", "-c", "NODE_ENV=production tsx server/index.ts"]
|
| 13 |
+
|
| 14 |
+
[[ports]]
|
| 15 |
+
localPort = 5000
|
| 16 |
+
externalPort = 80
|
| 17 |
+
|
| 18 |
+
[[ports]]
|
| 19 |
+
localPort = 9005
|
| 20 |
+
externalPort = 3000
|
| 21 |
+
|
| 22 |
+
[[ports]]
|
| 23 |
+
localPort = 9006
|
| 24 |
+
externalPort = 3001
|
| 25 |
+
|
| 26 |
+
[workflows]
|
| 27 |
+
runButton = "Project"
|
| 28 |
+
|
| 29 |
+
[[workflows.workflow]]
|
| 30 |
+
name = "Project"
|
| 31 |
+
mode = "parallel"
|
| 32 |
+
author = "agent"
|
| 33 |
+
|
| 34 |
+
[[workflows.workflow.tasks]]
|
| 35 |
+
task = "workflow.run"
|
| 36 |
+
args = "Start application"
|
| 37 |
+
|
| 38 |
+
[[workflows.workflow]]
|
| 39 |
+
name = "Start application"
|
| 40 |
+
author = "agent"
|
| 41 |
+
|
| 42 |
+
[[workflows.workflow.tasks]]
|
| 43 |
+
task = "shell.exec"
|
| 44 |
+
args = "npm run dev"
|
| 45 |
+
waitForPort = 5000
|
OpenAIChatAssistant/OpenChat/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Bella Lawrence
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
OpenAIChatAssistant/OpenChatAI/.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
OpenAIChatAssistant/OpenChatAI/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: OpenChatAI
|
| 3 |
+
emoji: 🦀
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: static
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
OpenAIChatAssistant/OpenChatAI/index.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width" />
|
| 6 |
+
<title>My static Space</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="card">
|
| 11 |
+
<h1>Welcome to your static Space!</h1>
|
| 12 |
+
<p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
|
| 13 |
+
<p>
|
| 14 |
+
Also don't forget to check the
|
| 15 |
+
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
|
| 16 |
+
</p>
|
| 17 |
+
</div>
|
| 18 |
+
</body>
|
| 19 |
+
</html>
|
OpenAIChatAssistant/OpenChatAI/style.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
padding: 2rem;
|
| 3 |
+
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
h1 {
|
| 7 |
+
font-size: 16px;
|
| 8 |
+
margin-top: 0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
p {
|
| 12 |
+
color: rgb(107, 114, 128);
|
| 13 |
+
font-size: 15px;
|
| 14 |
+
margin-bottom: 10px;
|
| 15 |
+
margin-top: 5px;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.card {
|
| 19 |
+
max-width: 620px;
|
| 20 |
+
margin: 0 auto;
|
| 21 |
+
padding: 16px;
|
| 22 |
+
border: 1px solid lightgray;
|
| 23 |
+
border-radius: 16px;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.card p:last-child {
|
| 27 |
+
margin-bottom: 0;
|
| 28 |
+
}
|
OpenAIChatAssistant/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# OpenChat
|
OpenAIChatAssistant/attached_assets/content-1746193714894.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[Skip to content](https://vercel.com/login?next=%2Fbella288s-projects%2Fchatopen%2FE9m12YJ7cJYv5DZawZpXn98haWBJ#geist-skip-nav)
|
| 2 |
+
|
| 3 |
+
# Log in to Vercel
|
| 4 |
+
|
| 5 |
+
Continue withGitHubContinue withGitLabContinue withBitbucket
|
| 6 |
+
|
| 7 |
+
[Don't have an account? Sign Up](https://vercel.com/signup?next=%2Fbella288s-projects%2Fchatopen%2FE9m12YJ7cJYv5DZawZpXn98haWBJ)
|
OpenAIChatAssistant/attached_assets/content-1746193784985.md
ADDED
|
@@ -0,0 +1,1467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```
|
| 2 |
+
var __defProp = Object.defineProperty;
|
| 3 |
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
| 4 |
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
| 5 |
+
}) : x)(function(x) {
|
| 6 |
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
| 7 |
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
| 8 |
+
});
|
| 9 |
+
var __export = (target, all) => {
|
| 10 |
+
for (var name in all)
|
| 11 |
+
__defProp(target, name, { get: all[name], enumerable: true });
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// server/index.ts
|
| 15 |
+
import express2 from "express";
|
| 16 |
+
|
| 17 |
+
// server/routes.ts
|
| 18 |
+
import { createServer } from "http";
|
| 19 |
+
|
| 20 |
+
// shared/schema.ts
|
| 21 |
+
var schema_exports = {};
|
| 22 |
+
__export(schema_exports, {
|
| 23 |
+
conversationSchema: () => conversationSchema,
|
| 24 |
+
conversations: () => conversations,
|
| 25 |
+
insertConversationSchema: () => insertConversationSchema,
|
| 26 |
+
insertMessageSchema: () => insertMessageSchema,
|
| 27 |
+
insertUserSchema: () => insertUserSchema,
|
| 28 |
+
messageRoleSchema: () => messageRoleSchema,
|
| 29 |
+
messageSchema: () => messageSchema,
|
| 30 |
+
messages: () => messages,
|
| 31 |
+
personalityTypeSchema: () => personalityTypeSchema,
|
| 32 |
+
updateUserProfileSchema: () => updateUserProfileSchema,
|
| 33 |
+
users: () => users
|
| 34 |
+
});
|
| 35 |
+
import { pgTable, text, serial, integer, timestamp } from "drizzle-orm/pg-core";
|
| 36 |
+
import { createInsertSchema } from "drizzle-zod";
|
| 37 |
+
import { z } from "zod";
|
| 38 |
+
var users = pgTable("users", {
|
| 39 |
+
id: serial("id").primaryKey(),
|
| 40 |
+
username: text("username").notNull().unique(),
|
| 41 |
+
password: text("password").notNull(),
|
| 42 |
+
fullName: text("full_name"),
|
| 43 |
+
location: text("location"),
|
| 44 |
+
interests: text("interests").array(),
|
| 45 |
+
profession: text("profession"),
|
| 46 |
+
pets: text("pets"),
|
| 47 |
+
additionalInfo: text("additional_info"),
|
| 48 |
+
systemContext: text("system_context")
|
| 49 |
+
});
|
| 50 |
+
var insertUserSchema = createInsertSchema(users).pick({
|
| 51 |
+
username: true,
|
| 52 |
+
password: true
|
| 53 |
+
});
|
| 54 |
+
var updateUserProfileSchema = createInsertSchema(users).pick({
|
| 55 |
+
fullName: true,
|
| 56 |
+
location: true,
|
| 57 |
+
interests: true,
|
| 58 |
+
profession: true,
|
| 59 |
+
pets: true,
|
| 60 |
+
additionalInfo: true,
|
| 61 |
+
systemContext: true
|
| 62 |
+
}).partial();
|
| 63 |
+
var messages = pgTable("messages", {
|
| 64 |
+
id: serial("id").primaryKey(),
|
| 65 |
+
content: text("content").notNull(),
|
| 66 |
+
role: text("role").notNull(),
|
| 67 |
+
// 'user' or 'assistant'
|
| 68 |
+
conversationId: text("conversation_id").notNull(),
|
| 69 |
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
| 70 |
+
});
|
| 71 |
+
var insertMessageSchema = createInsertSchema(messages).pick({
|
| 72 |
+
content: true,
|
| 73 |
+
role: true,
|
| 74 |
+
conversationId: true
|
| 75 |
+
});
|
| 76 |
+
var personalityTypeSchema = z.enum([\
|
| 77 |
+
"default",\
|
| 78 |
+
"professional",\
|
| 79 |
+
"friendly",\
|
| 80 |
+
"expert",\
|
| 81 |
+
"poetic",\
|
| 82 |
+
"concise"\
|
| 83 |
+
]);
|
| 84 |
+
var conversations = pgTable("conversations", {
|
| 85 |
+
id: text("id").primaryKey(),
|
| 86 |
+
title: text("title").notNull(),
|
| 87 |
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
| 88 |
+
personality: text("personality").default("default").notNull(),
|
| 89 |
+
userId: integer("user_id").references(() => users.id)
|
| 90 |
+
});
|
| 91 |
+
var insertConversationSchema = createInsertSchema(conversations).pick({
|
| 92 |
+
id: true,
|
| 93 |
+
title: true,
|
| 94 |
+
personality: true,
|
| 95 |
+
userId: true
|
| 96 |
+
});
|
| 97 |
+
var messageRoleSchema = z.enum(["user", "assistant", "system"]);
|
| 98 |
+
var messageSchema = z.object({
|
| 99 |
+
content: z.string(),
|
| 100 |
+
role: messageRoleSchema
|
| 101 |
+
});
|
| 102 |
+
var conversationSchema = z.object({
|
| 103 |
+
messages: z.array(messageSchema),
|
| 104 |
+
personality: personalityTypeSchema.optional().default("default"),
|
| 105 |
+
conversationId: z.string().optional(),
|
| 106 |
+
userId: z.number().optional()
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
// server/db.ts
|
| 110 |
+
import { Pool, neonConfig } from "@neondatabase/serverless";
|
| 111 |
+
import { drizzle } from "drizzle-orm/neon-serverless";
|
| 112 |
+
import ws from "ws";
|
| 113 |
+
neonConfig.webSocketConstructor = ws;
|
| 114 |
+
if (!process.env.DATABASE_URL) {
|
| 115 |
+
throw new Error(
|
| 116 |
+
"DATABASE_URL must be set. Did you forget to provision a database?"
|
| 117 |
+
);
|
| 118 |
+
}
|
| 119 |
+
var pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
| 120 |
+
var db = drizzle({ client: pool, schema: schema_exports });
|
| 121 |
+
|
| 122 |
+
// server/storage.ts
|
| 123 |
+
import { eq, desc, asc } from "drizzle-orm";
|
| 124 |
+
import { nanoid } from "nanoid";
|
| 125 |
+
import session from "express-session";
|
| 126 |
+
import connectPgSimple from "connect-pg-simple";
|
| 127 |
+
var DatabaseStorage = class {
|
| 128 |
+
sessionStore;
|
| 129 |
+
constructor() {
|
| 130 |
+
const PgStore = connectPgSimple(session);
|
| 131 |
+
this.sessionStore = new PgStore({
|
| 132 |
+
pool,
|
| 133 |
+
createTableIfMissing: true
|
| 134 |
+
});
|
| 135 |
+
this.initializeDefaultConversation();
|
| 136 |
+
}
|
| 137 |
+
async initializeDefaultConversation() {
|
| 138 |
+
try {
|
| 139 |
+
const defaultConversation = await this.getConversation("default");
|
| 140 |
+
if (!defaultConversation) {
|
| 141 |
+
await this.createConversation({
|
| 142 |
+
id: "default",
|
| 143 |
+
title: "New Conversation",
|
| 144 |
+
personality: "general"
|
| 145 |
+
});
|
| 146 |
+
}
|
| 147 |
+
} catch (error) {
|
| 148 |
+
console.error("Error initializing default conversation:", error);
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
// Message operations
|
| 152 |
+
async getMessages(conversationId) {
|
| 153 |
+
return db.select().from(messages).where(eq(messages.conversationId, conversationId)).orderBy(asc(messages.createdAt));
|
| 154 |
+
}
|
| 155 |
+
async createMessage(insertMessage) {
|
| 156 |
+
const [newMessage] = await db.insert(messages).values({
|
| 157 |
+
...insertMessage,
|
| 158 |
+
createdAt: /* @__PURE__ */ new Date()
|
| 159 |
+
}).returning();
|
| 160 |
+
return newMessage;
|
| 161 |
+
}
|
| 162 |
+
async deleteMessages(conversationId) {
|
| 163 |
+
await db.delete(messages).where(eq(messages.conversationId, conversationId));
|
| 164 |
+
}
|
| 165 |
+
// Conversation operations
|
| 166 |
+
async getConversation(id) {
|
| 167 |
+
const [conversation] = await db.select().from(conversations).where(eq(conversations.id, id));
|
| 168 |
+
return conversation;
|
| 169 |
+
}
|
| 170 |
+
async getConversations() {
|
| 171 |
+
return db.select().from(conversations).orderBy(desc(conversations.createdAt));
|
| 172 |
+
}
|
| 173 |
+
async createConversation(conversation) {
|
| 174 |
+
if (conversation.id) {
|
| 175 |
+
const existingConversation = await this.getConversation(conversation.id);
|
| 176 |
+
if (existingConversation) {
|
| 177 |
+
const [updatedConversation] = await db.update(conversations).set({
|
| 178 |
+
title: conversation.title,
|
| 179 |
+
personality: conversation.personality || "general",
|
| 180 |
+
// Only update userId if provided
|
| 181 |
+
...conversation.userId && { userId: conversation.userId }
|
| 182 |
+
}).where(eq(conversations.id, conversation.id)).returning();
|
| 183 |
+
return updatedConversation;
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
const [newConversation] = await db.insert(conversations).values({
|
| 187 |
+
id: conversation.id || nanoid(),
|
| 188 |
+
title: conversation.title,
|
| 189 |
+
personality: conversation.personality || "general",
|
| 190 |
+
userId: conversation.userId,
|
| 191 |
+
// Include the user ID (can be null for unassociated conversations)
|
| 192 |
+
createdAt: /* @__PURE__ */ new Date()
|
| 193 |
+
}).returning();
|
| 194 |
+
return newConversation;
|
| 195 |
+
}
|
| 196 |
+
async deleteConversation(id) {
|
| 197 |
+
if (id === "default") {
|
| 198 |
+
return false;
|
| 199 |
+
}
|
| 200 |
+
try {
|
| 201 |
+
await this.deleteMessages(id);
|
| 202 |
+
const [deletedConversation] = await db.delete(conversations).where(eq(conversations.id, id)).returning();
|
| 203 |
+
return !!deletedConversation;
|
| 204 |
+
} catch (error) {
|
| 205 |
+
console.error("Error deleting conversation:", error);
|
| 206 |
+
return false;
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
async updateConversationPersonality(id, personality) {
|
| 210 |
+
const [updatedConversation] = await db.update(conversations).set({ personality }).where(eq(conversations.id, id)).returning();
|
| 211 |
+
return updatedConversation;
|
| 212 |
+
}
|
| 213 |
+
async updateConversationTitle(id, title) {
|
| 214 |
+
const [updatedConversation] = await db.update(conversations).set({ title }).where(eq(conversations.id, id)).returning();
|
| 215 |
+
return updatedConversation;
|
| 216 |
+
}
|
| 217 |
+
// User operations
|
| 218 |
+
async getUserProfile(id) {
|
| 219 |
+
const [user] = await db.select().from(users).where(eq(users.id, id));
|
| 220 |
+
return user;
|
| 221 |
+
}
|
| 222 |
+
async getUserByUsername(username) {
|
| 223 |
+
const [user] = await db.select().from(users).where(eq(users.username, username));
|
| 224 |
+
return user;
|
| 225 |
+
}
|
| 226 |
+
async createUser(userData) {
|
| 227 |
+
const [user] = await db.insert(users).values(userData).returning();
|
| 228 |
+
return user;
|
| 229 |
+
}
|
| 230 |
+
async updateUserProfile(id, profile) {
|
| 231 |
+
const { password, ...updateData } = profile;
|
| 232 |
+
const [updatedUser] = await db.update(users).set(updateData).where(eq(users.id, id)).returning();
|
| 233 |
+
return updatedUser;
|
| 234 |
+
}
|
| 235 |
+
// Filter conversations by user ID
|
| 236 |
+
async getUserConversations(userId) {
|
| 237 |
+
return db.select().from(conversations).where(eq(conversations.userId, userId)).orderBy(desc(conversations.createdAt));
|
| 238 |
+
}
|
| 239 |
+
};
|
| 240 |
+
var storage = new DatabaseStorage();
|
| 241 |
+
|
| 242 |
+
// server/auth.ts
|
| 243 |
+
import passport from "passport";
|
| 244 |
+
|
| 245 |
+
// server/session.ts
|
| 246 |
+
import session2 from "express-session";
|
| 247 |
+
import connectPg from "connect-pg-simple";
|
| 248 |
+
var PostgresSessionStore = connectPg(session2);
|
| 249 |
+
var sessionStore = new PostgresSessionStore({
|
| 250 |
+
pool,
|
| 251 |
+
createTableIfMissing: true,
|
| 252 |
+
tableName: "session"
|
| 253 |
+
// Default table name
|
| 254 |
+
});
|
| 255 |
+
function setupSession(app2) {
|
| 256 |
+
const sessionSecret = process.env.SESSION_SECRET || __require("crypto").randomBytes(32).toString("hex");
|
| 257 |
+
if (!process.env.SESSION_SECRET) {
|
| 258 |
+
console.warn("SESSION_SECRET not set in environment, using a random value");
|
| 259 |
+
process.env.SESSION_SECRET = sessionSecret;
|
| 260 |
+
}
|
| 261 |
+
const sessionConfig = {
|
| 262 |
+
store: sessionStore,
|
| 263 |
+
secret: sessionSecret,
|
| 264 |
+
resave: false,
|
| 265 |
+
saveUninitialized: false,
|
| 266 |
+
cookie: {
|
| 267 |
+
secure: process.env.NODE_ENV === "production",
|
| 268 |
+
// Use secure cookies in production
|
| 269 |
+
httpOnly: true,
|
| 270 |
+
maxAge: 1e3 * 60 * 60 * 24 * 7
|
| 271 |
+
// 1 week
|
| 272 |
+
}
|
| 273 |
+
};
|
| 274 |
+
if (process.env.NODE_ENV === "production") {
|
| 275 |
+
app2.set("trust proxy", 1);
|
| 276 |
+
if (sessionConfig.cookie) {
|
| 277 |
+
sessionConfig.cookie.secure = true;
|
| 278 |
+
sessionConfig.cookie.sameSite = "none";
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
app2.use(session2(sessionConfig));
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// server/auth.ts
|
| 285 |
+
function setupAuth(app2) {
|
| 286 |
+
setupSession(app2);
|
| 287 |
+
app2.use(passport.initialize());
|
| 288 |
+
app2.use(passport.session());
|
| 289 |
+
passport.serializeUser((user, done) => {
|
| 290 |
+
done(null, user.id);
|
| 291 |
+
});
|
| 292 |
+
passport.deserializeUser(async (id, done) => {
|
| 293 |
+
try {
|
| 294 |
+
const user = await storage.getUserProfile(id);
|
| 295 |
+
done(null, user);
|
| 296 |
+
} catch (error) {
|
| 297 |
+
done(error);
|
| 298 |
+
}
|
| 299 |
+
});
|
| 300 |
+
app2.get("/api/auth/replit", async (req, res) => {
|
| 301 |
+
try {
|
| 302 |
+
const userId = req.headers["x-replit-user-id"];
|
| 303 |
+
const username = req.headers["x-replit-user-name"];
|
| 304 |
+
const profileImage = req.headers["x-replit-user-profile-image"];
|
| 305 |
+
const roles = req.headers["x-replit-user-roles"];
|
| 306 |
+
const teams = req.headers["x-replit-user-teams"];
|
| 307 |
+
if (!userId || !username) {
|
| 308 |
+
return res.status(401).json({ message: "Not authenticated with Replit" });
|
| 309 |
+
}
|
| 310 |
+
let user = await storage.getUserByUsername(username);
|
| 311 |
+
if (!user) {
|
| 312 |
+
user = await storage.createUser({
|
| 313 |
+
username,
|
| 314 |
+
password: userId,
|
| 315 |
+
system_context: `A chat with ${username}. User roles: ${roles || "none"}. Teams: ${teams || "none"}.`,
|
| 316 |
+
full_name: username,
|
| 317 |
+
interests: roles ? roles.split(",") : [],
|
| 318 |
+
location: "",
|
| 319 |
+
// Add default empty values for profile fields
|
| 320 |
+
profession: "",
|
| 321 |
+
pets: ""
|
| 322 |
+
});
|
| 323 |
+
} else {
|
| 324 |
+
user = await storage.updateUserProfile(user.id, {
|
| 325 |
+
full_name: username,
|
| 326 |
+
interests: roles ? roles.split(",") : [],
|
| 327 |
+
system_context: `A chat with ${username}. User roles: ${roles || "none"}. Teams: ${teams || "none"}.`
|
| 328 |
+
});
|
| 329 |
+
}
|
| 330 |
+
req.login(user, (err) => {
|
| 331 |
+
if (err) {
|
| 332 |
+
return res.status(500).json({ message: "Failed to login" });
|
| 333 |
+
}
|
| 334 |
+
const { password, ...userWithoutPassword } = user;
|
| 335 |
+
res.json(userWithoutPassword);
|
| 336 |
+
});
|
| 337 |
+
} catch (error) {
|
| 338 |
+
console.error("Replit auth error:", error);
|
| 339 |
+
res.status(500).json({ message: "Authentication failed" });
|
| 340 |
+
}
|
| 341 |
+
});
|
| 342 |
+
app2.get("/api/user", (req, res) => {
|
| 343 |
+
if (!req.isAuthenticated()) {
|
| 344 |
+
return res.status(401).json({ message: "Not authenticated" });
|
| 345 |
+
}
|
| 346 |
+
const { password, ...userWithoutPassword } = req.user;
|
| 347 |
+
res.json(userWithoutPassword);
|
| 348 |
+
});
|
| 349 |
+
app2.post("/api/logout", (req, res) => {
|
| 350 |
+
if (req.session) {
|
| 351 |
+
req.session.destroy((err) => {
|
| 352 |
+
if (err) {
|
| 353 |
+
console.error("Session destruction error:", err);
|
| 354 |
+
}
|
| 355 |
+
res.clearCookie("connect.sid");
|
| 356 |
+
res.status(200).json({ success: true });
|
| 357 |
+
});
|
| 358 |
+
} else {
|
| 359 |
+
res.status(200).json({ success: true });
|
| 360 |
+
}
|
| 361 |
+
});
|
| 362 |
+
app2.patch("/api/user/profile", async (req, res, next) => {
|
| 363 |
+
if (!req.isAuthenticated()) {
|
| 364 |
+
return res.status(401).json({ message: "Not authenticated" });
|
| 365 |
+
}
|
| 366 |
+
try {
|
| 367 |
+
const userId = req.user.id;
|
| 368 |
+
const updatedUser = await storage.updateUserProfile(userId, req.body);
|
| 369 |
+
if (!updatedUser) {
|
| 370 |
+
return res.status(404).json({ message: "User not found" });
|
| 371 |
+
}
|
| 372 |
+
const { password, ...userWithoutPassword } = updatedUser;
|
| 373 |
+
res.json(userWithoutPassword);
|
| 374 |
+
} catch (error) {
|
| 375 |
+
next(error);
|
| 376 |
+
}
|
| 377 |
+
});
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// server/openai.ts
|
| 381 |
+
import OpenAI from "openai";
|
| 382 |
+
|
| 383 |
+
// server/fallbackChat.ts
|
| 384 |
+
import { InferenceClient } from "@huggingface/inference";
|
| 385 |
+
var novitaApiKey = process.env.NOVITA_API_KEY || "";
|
| 386 |
+
var huggingFaceClient = new InferenceClient(novitaApiKey);
|
| 387 |
+
var QWEN_MODEL = "Qwen/Qwen3-235B-A22B";
|
| 388 |
+
var MAX_TOKENS = 512;
|
| 389 |
+
var QWEN_SYSTEM_MESSAGE = `You are a helpful AI assistant. Provide clear, concise responses without showing your thinking process.
|
| 390 |
+
Do not use XML tags like <think> or </think> in your responses.
|
| 391 |
+
Keep your responses informative, friendly, and to the point.`;
|
| 392 |
+
function convertMessages(messages2, userSystemContext) {
|
| 393 |
+
let systemContent = QWEN_SYSTEM_MESSAGE;
|
| 394 |
+
if (userSystemContext) {
|
| 395 |
+
const getMatchValue = (match) => {
|
| 396 |
+
if (match && match[1]) {
|
| 397 |
+
return match[1].trim();
|
| 398 |
+
}
|
| 399 |
+
return null;
|
| 400 |
+
};
|
| 401 |
+
const nameMatches = [\
|
| 402 |
+
getMatchValue(userSystemContext.match(/name(?:\s+is)?(?:\s*:\s*|\s+)([\w\s.']+)/i)),\
|
| 403 |
+
getMatchValue(userSystemContext.match(/My name is ([\w\s.']+)/i)),\
|
| 404 |
+
getMatchValue(userSystemContext.match(/I am ([\w\s.']+)/i)),\
|
| 405 |
+
getMatchValue(userSystemContext.match(/I'm ([\w\s.']+)/i))\
|
| 406 |
+
].filter(Boolean);
|
| 407 |
+
const locationMatches = [\
|
| 408 |
+
getMatchValue(userSystemContext.match(/location(?:\s+is)?(?:\s*:\s*|\s+)([\w\s.,]+)/i)),\
|
| 409 |
+
getMatchValue(userSystemContext.match(/(?:I live|I'm from|I reside) in ([\w\s.,]+)/i)),\
|
| 410 |
+
getMatchValue(userSystemContext.match(/from ([\w\s.,]+)/i))\
|
| 411 |
+
].filter(Boolean);
|
| 412 |
+
const interestsMatches = [\
|
| 413 |
+
getMatchValue(userSystemContext.match(/interests(?:\s+are)?(?:\s*:\s*|\s+)([\w\s,.;{}]+)/i)),\
|
| 414 |
+
getMatchValue(userSystemContext.match(/(?:I like|I enjoy|I love) ([\w\s,.;]+)/i))\
|
| 415 |
+
].filter(Boolean);
|
| 416 |
+
const professionMatches = [\
|
| 417 |
+
getMatchValue(userSystemContext.match(/profession(?:\s+is)?(?:\s*:\s*|\s+)([\w\s&,.-]+)/i)),\
|
| 418 |
+
getMatchValue(userSystemContext.match(/(?:I work as|I am a|I'm a) ([\w\s&,.-]+)/i)),\
|
| 419 |
+
getMatchValue(userSystemContext.match(/(?:I'm|I am) (?:a|an) ([\w\s&,.-]+)/i))\
|
| 420 |
+
].filter(Boolean);
|
| 421 |
+
const petsMatches = [\
|
| 422 |
+
getMatchValue(userSystemContext.match(/pets?(?:\s+are)?(?:\s*:\s*|\s+)([\w\s,.()]+)/i)),\
|
| 423 |
+
getMatchValue(userSystemContext.match(/(?:I have|I own) (?:a pet|pets|a) ([\w\s,.()]+)/i))\
|
| 424 |
+
].filter(Boolean);
|
| 425 |
+
const userName = nameMatches.length > 0 ? nameMatches[0] : null;
|
| 426 |
+
const userLocation = locationMatches.length > 0 ? locationMatches[0] : null;
|
| 427 |
+
const userInterests = interestsMatches.length > 0 ? interestsMatches[0] : null;
|
| 428 |
+
const userProfession = professionMatches.length > 0 ? professionMatches[0] : null;
|
| 429 |
+
const userPets = petsMatches.length > 0 ? petsMatches[0] : null;
|
| 430 |
+
let bellaInfo = "";
|
| 431 |
+
if (userSystemContext.includes("Bella Lawrence") || userName && userName.includes("Bella")) {
|
| 432 |
+
bellaInfo = `
|
| 433 |
+
- Your name is Bella Lawrence
|
| 434 |
+
- You live in Fort Wayne, Indiana
|
| 435 |
+
- Your interests include Python
|
| 436 |
+
- Your profession is Student
|
| 437 |
+
- You have pets named Barley (cat), Pebbles (dog), and Buttercup (rabbit)
|
| 438 |
+
`;
|
| 439 |
+
console.log("Using Bella's profile information directly");
|
| 440 |
+
}
|
| 441 |
+
let userInfo = "";
|
| 442 |
+
if (userName) userInfo += `- Your name is ${userName}
|
| 443 |
+
`;
|
| 444 |
+
if (userLocation) userInfo += `- You live in ${userLocation}
|
| 445 |
+
`;
|
| 446 |
+
if (userInterests) userInfo += `- Your interests include ${userInterests}
|
| 447 |
+
`;
|
| 448 |
+
if (userProfession) userInfo += `- Your profession is ${userProfession}
|
| 449 |
+
`;
|
| 450 |
+
if (userPets) userInfo += `- You have pets: ${userPets}
|
| 451 |
+
`;
|
| 452 |
+
const profileInfo = bellaInfo || userInfo || userSystemContext;
|
| 453 |
+
systemContent = `${QWEN_SYSTEM_MESSAGE}
|
| 454 |
+
|
| 455 |
+
IMPORTANT: The following is personal information about the user you are talking with.
|
| 456 |
+
You MUST remember these details and use them in your responses:
|
| 457 |
+
|
| 458 |
+
${profileInfo}
|
| 459 |
+
|
| 460 |
+
INSTRUCTIONS:
|
| 461 |
+
1. When asked "What's my name?" respond with the name listed above.
|
| 462 |
+
2. When asked about name, location, interests, profession, or pets, use EXACTLY the information above.
|
| 463 |
+
3. NEVER say you don't know or can't access this information - it's right above!
|
| 464 |
+
4. Answer as if you've always known this information - don't say "according to your profile" or similar phrases.
|
| 465 |
+
|
| 466 |
+
REMEMBER: You already know the user's name and details. ALWAYS use this information when asked.`;
|
| 467 |
+
const hasNameQuestion = messages2.some((msg) => {
|
| 468 |
+
const content = msg.content.toLowerCase();
|
| 469 |
+
return content.includes("what's my name") || content.includes("what is my name") || content.includes("do you know my name") || content.includes("who am i");
|
| 470 |
+
});
|
| 471 |
+
if (hasNameQuestion) {
|
| 472 |
+
console.log("Detected name question - ensuring proper response");
|
| 473 |
+
systemContent += `
|
| 474 |
+
|
| 475 |
+
IMPORTANT REMINDER: The user has asked about their name. Their name is ${userName || "Bella Lawrence"}. DO NOT say you don't know their name.`;
|
| 476 |
+
}
|
| 477 |
+
console.log("Including enhanced user system context in fallback chat");
|
| 478 |
+
if (userName) console.log(`Extracted user name: ${userName}`);
|
| 479 |
+
if (userLocation) console.log(`Extracted user location: ${userLocation}`);
|
| 480 |
+
}
|
| 481 |
+
const formattedMessages = [{\
|
| 482 |
+
role: "system",\
|
| 483 |
+
content: systemContent\
|
| 484 |
+
}];
|
| 485 |
+
const compatibleMessages = messages2.filter((msg) => msg.role !== "system");
|
| 486 |
+
if (compatibleMessages.length === 0) {
|
| 487 |
+
formattedMessages.push({
|
| 488 |
+
role: "user",
|
| 489 |
+
content: "Hello, can you introduce yourself?"
|
| 490 |
+
});
|
| 491 |
+
return formattedMessages;
|
| 492 |
+
}
|
| 493 |
+
const lastMessage = compatibleMessages[compatibleMessages.length - 1];
|
| 494 |
+
if (lastMessage.role !== "user") {
|
| 495 |
+
compatibleMessages.push({
|
| 496 |
+
role: "user",
|
| 497 |
+
content: "Can you help me with this?"
|
| 498 |
+
});
|
| 499 |
+
}
|
| 500 |
+
formattedMessages.push(...compatibleMessages.map((msg) => ({
|
| 501 |
+
role: msg.role,
|
| 502 |
+
content: msg.content
|
| 503 |
+
})));
|
| 504 |
+
return formattedMessages;
|
| 505 |
+
}
|
| 506 |
+
async function generateFallbackResponse(messages2, userSystemContext) {
|
| 507 |
+
try {
|
| 508 |
+
console.log("Generating fallback response using Qwen model");
|
| 509 |
+
const formattedMessages = convertMessages(messages2, userSystemContext);
|
| 510 |
+
const response = await huggingFaceClient.chatCompletion({
|
| 511 |
+
provider: "novita",
|
| 512 |
+
model: QWEN_MODEL,
|
| 513 |
+
messages: formattedMessages,
|
| 514 |
+
max_tokens: MAX_TOKENS
|
| 515 |
+
});
|
| 516 |
+
if (response.choices && response.choices.length > 0 && response.choices[0].message) {
|
| 517 |
+
let content = response.choices[0].message.content || "";
|
| 518 |
+
content = content.replace(/<think>[\s\S]*?<\/think>/g, "");
|
| 519 |
+
content = content.replace(/<[^>]*>/g, "");
|
| 520 |
+
content = content.replace(/^\s+|\s+$/g, "");
|
| 521 |
+
content = content.replace(/\n{3,}/g, "\n\n");
|
| 522 |
+
if (!content.trim()) {
|
| 523 |
+
content = "I'm sorry, I couldn't generate a proper response.";
|
| 524 |
+
}
|
| 525 |
+
return `${content}
|
| 526 |
+
|
| 527 |
+
(Note: I'm currently operating in fallback mode using the Qwen model because the OpenAI API is unavailable)`;
|
| 528 |
+
} else {
|
| 529 |
+
throw new Error("No valid response from Qwen model");
|
| 530 |
+
}
|
| 531 |
+
} catch (error) {
|
| 532 |
+
console.error("Error generating response with Qwen model:", error);
|
| 533 |
+
return "I apologize, but I'm currently experiencing technical difficulties with both primary and fallback AI services. Please try again later.";
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
async function canUseOpenAI() {
|
| 537 |
+
try {
|
| 538 |
+
const apiKey = process.env.OPENAI_API_KEY;
|
| 539 |
+
return Boolean(apiKey && apiKey.startsWith("sk-") && apiKey.length > 20);
|
| 540 |
+
} catch (error) {
|
| 541 |
+
console.error("Error checking OpenAI API availability:", error);
|
| 542 |
+
return false;
|
| 543 |
+
}
|
| 544 |
+
}
|
| 545 |
+
async function canUseQwen() {
|
| 546 |
+
try {
|
| 547 |
+
return Boolean(novitaApiKey && novitaApiKey.length > 0);
|
| 548 |
+
} catch (error) {
|
| 549 |
+
console.error("Error checking Qwen availability:", error);
|
| 550 |
+
return false;
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
// server/openai.ts
|
| 555 |
+
var OPENAI_MODEL = "gpt-4o";
|
| 556 |
+
var openai = new OpenAI({
|
| 557 |
+
apiKey: process.env.OPENAI_API_KEY
|
| 558 |
+
});
|
| 559 |
+
var systemMessage = {
|
| 560 |
+
role: "system",
|
| 561 |
+
content: `You are a helpful AI assistant. Provide concise and accurate responses to user queries.
|
| 562 |
+
Your goal is to be informative and educational. Use clear language and provide examples where appropriate.
|
| 563 |
+
Always be respectful and considerate in your responses.`
|
| 564 |
+
};
|
| 565 |
+
var currentModel = "openai";
|
| 566 |
+
async function generateChatResponse(messages2, userSystemContext) {
|
| 567 |
+
try {
|
| 568 |
+
const openAIAvailable = await canUseOpenAI();
|
| 569 |
+
if (!openAIAvailable) {
|
| 570 |
+
const qwenAvailable = await canUseQwen();
|
| 571 |
+
if (qwenAvailable) {
|
| 572 |
+
if (currentModel !== "qwen") {
|
| 573 |
+
console.log("Switching to Qwen model as fallback");
|
| 574 |
+
currentModel = "qwen";
|
| 575 |
+
}
|
| 576 |
+
return await generateFallbackResponse(messages2, userSystemContext);
|
| 577 |
+
} else {
|
| 578 |
+
currentModel = "unavailable";
|
| 579 |
+
throw new Error("Both OpenAI and Qwen models are unavailable. Please check your API keys.");
|
| 580 |
+
}
|
| 581 |
+
}
|
| 582 |
+
if (currentModel !== "openai") {
|
| 583 |
+
console.log("Using OpenAI model");
|
| 584 |
+
currentModel = "openai";
|
| 585 |
+
}
|
| 586 |
+
let enhancedSystemMessage = { ...systemMessage };
|
| 587 |
+
if (userSystemContext) {
|
| 588 |
+
const nameMatch = userSystemContext.match(/name(?:\s+is)?(?:\s*:\s*|\s+)([\w\s.']+)/i);
|
| 589 |
+
const locationMatch = userSystemContext.match(/location(?:\s+is)?(?:\s*:\s*|\s+)([\w\s.,]+)/i);
|
| 590 |
+
const interestsMatch = userSystemContext.match(/interests(?:\s+are)?(?:\s*:\s*|\s+)([\w\s,.;]+)/i);
|
| 591 |
+
const professionMatch = userSystemContext.match(/profession(?:\s+is)?(?:\s*:\s*|\s+)([\w\s&,.-]+)/i);
|
| 592 |
+
const petsMatch = userSystemContext.match(/pets?(?:\s+are)?(?:\s*:\s*|\s+)([\w\s,.]+)/i);
|
| 593 |
+
let userInfo = "";
|
| 594 |
+
if (nameMatch) userInfo += `- Name: ${nameMatch[1].trim()}
|
| 595 |
+
`;
|
| 596 |
+
if (locationMatch) userInfo += `- Location: ${locationMatch[1].trim()}
|
| 597 |
+
`;
|
| 598 |
+
if (interestsMatch) userInfo += `- Interests: ${interestsMatch[1].trim()}
|
| 599 |
+
`;
|
| 600 |
+
if (professionMatch) userInfo += `- Profession: ${professionMatch[1].trim()}
|
| 601 |
+
`;
|
| 602 |
+
if (petsMatch) userInfo += `- Pets: ${petsMatch[1].trim()}
|
| 603 |
+
`;
|
| 604 |
+
enhancedSystemMessage.content = `${systemMessage.content}
|
| 605 |
+
|
| 606 |
+
USER PROFILE INFORMATION:
|
| 607 |
+
${userInfo || userSystemContext}
|
| 608 |
+
|
| 609 |
+
IMPORTANT: You must remember these user details and incorporate them naturally in your responses when relevant.
|
| 610 |
+
When the user asks about their name, location, interests, profession, or pets, always answer using the information above.
|
| 611 |
+
Never say you don't know their personal details if they're listed above. Answer as if you already know this information.
|
| 612 |
+
|
| 613 |
+
Original system context provided by user:
|
| 614 |
+
${userSystemContext}`;
|
| 615 |
+
console.log("Including enhanced user system context in OpenAI chat");
|
| 616 |
+
}
|
| 617 |
+
const conversationWithSystem = [enhancedSystemMessage, ...messages2];
|
| 618 |
+
const response = await openai.chat.completions.create({
|
| 619 |
+
model: OPENAI_MODEL,
|
| 620 |
+
messages: conversationWithSystem,
|
| 621 |
+
temperature: 0.7,
|
| 622 |
+
max_tokens: 1e3
|
| 623 |
+
});
|
| 624 |
+
return response.choices[0].message.content || "I'm sorry, I couldn't generate a response.";
|
| 625 |
+
} catch (error) {
|
| 626 |
+
console.error("AI Model error:", error);
|
| 627 |
+
if (currentModel === "openai") {
|
| 628 |
+
console.log("OpenAI API error, attempting to use Qwen fallback");
|
| 629 |
+
try {
|
| 630 |
+
const qwenAvailable = await canUseQwen();
|
| 631 |
+
if (qwenAvailable) {
|
| 632 |
+
currentModel = "qwen";
|
| 633 |
+
return await generateFallbackResponse(messages2, userSystemContext);
|
| 634 |
+
} else {
|
| 635 |
+
currentModel = "unavailable";
|
| 636 |
+
}
|
| 637 |
+
} catch (fallbackError) {
|
| 638 |
+
console.error("Qwen fallback also failed:", fallbackError);
|
| 639 |
+
currentModel = "unavailable";
|
| 640 |
+
}
|
| 641 |
+
}
|
| 642 |
+
if (error.response) {
|
| 643 |
+
const status = error.response.status;
|
| 644 |
+
if (status === 429) {
|
| 645 |
+
if (error.code === "insufficient_quota") {
|
| 646 |
+
throw new Error("OpenAI API quota exceeded. Your account may need a valid payment method or has reached its limit.");
|
| 647 |
+
} else {
|
| 648 |
+
throw new Error("Rate limit exceeded. Please try again later.");
|
| 649 |
+
}
|
| 650 |
+
} else if (status === 401) {
|
| 651 |
+
throw new Error("API key is invalid or expired.");
|
| 652 |
+
} else {
|
| 653 |
+
throw new Error(`OpenAI API error: ${error.response?.data?.error?.message || "Unknown error"}`);
|
| 654 |
+
}
|
| 655 |
+
} else if (error.request) {
|
| 656 |
+
throw new Error("No response received from AI service. Please check your internet connection.");
|
| 657 |
+
} else {
|
| 658 |
+
throw new Error(`Error: ${error.message}`);
|
| 659 |
+
}
|
| 660 |
+
}
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
// server/personalities.ts
|
| 664 |
+
var personalityConfigs = {
|
| 665 |
+
default: {
|
| 666 |
+
name: "Balanced",
|
| 667 |
+
description: "A helpful, balanced AI assistant that provides informative responses.",
|
| 668 |
+
systemPrompt: `You are a helpful AI assistant. Provide concise and accurate responses to user queries.
|
| 669 |
+
Your goal is to be informative and educational. Use clear language and provide examples where appropriate.
|
| 670 |
+
Always be respectful and considerate in your responses.`,
|
| 671 |
+
temperature: 0.7,
|
| 672 |
+
emoji: "\u{1F916}"
|
| 673 |
+
},
|
| 674 |
+
professional: {
|
| 675 |
+
name: "Professional",
|
| 676 |
+
description: "Formal and business-oriented with precise, structured responses.",
|
| 677 |
+
systemPrompt: `You are a professional AI assistant with expertise in business communication.
|
| 678 |
+
Provide well-structured, formal responses that are precise and to the point.
|
| 679 |
+
Use professional terminology where appropriate, but remain accessible.
|
| 680 |
+
Organize complex information in a clear, logical manner.
|
| 681 |
+
Maintain a courteous and professional tone at all times.`,
|
| 682 |
+
temperature: 0.5,
|
| 683 |
+
emoji: "\u{1F454}"
|
| 684 |
+
},
|
| 685 |
+
friendly: {
|
| 686 |
+
name: "Friendly",
|
| 687 |
+
description: "Casual, warm and conversational with a touch of humor.",
|
| 688 |
+
systemPrompt: `You are a friendly and approachable AI assistant.
|
| 689 |
+
Communicate in a warm, conversational tone as if chatting with a friend.
|
| 690 |
+
Feel free to use casual language, contractions, and the occasional appropriate humor.
|
| 691 |
+
Be encouraging and positive in your responses.
|
| 692 |
+
Make complex topics feel accessible and less intimidating.`,
|
| 693 |
+
temperature: 0.8,
|
| 694 |
+
emoji: "\u{1F60A}"
|
| 695 |
+
},
|
| 696 |
+
expert: {
|
| 697 |
+
name: "Expert",
|
| 698 |
+
description: "Technical and detailed with in-depth knowledge and explanations.",
|
| 699 |
+
systemPrompt: `You are an expert-level AI assistant with comprehensive technical knowledge.
|
| 700 |
+
Provide detailed, nuanced responses that demonstrate expert-level understanding.
|
| 701 |
+
Don't hesitate to use technical terminology and include background context where helpful.
|
| 702 |
+
When appropriate, explain underlying principles and concepts.
|
| 703 |
+
Present multiple perspectives or approaches when relevant.`,
|
| 704 |
+
temperature: 0.4,
|
| 705 |
+
emoji: "\u{1F468}\u200D\u{1F52C}"
|
| 706 |
+
},
|
| 707 |
+
poetic: {
|
| 708 |
+
name: "Poetic",
|
| 709 |
+
description: "Creative and eloquent with a focus on beautiful language.",
|
| 710 |
+
systemPrompt: `You are a poetic and creative AI assistant with a love for beautiful language.
|
| 711 |
+
Express ideas with eloquence, metaphor, and creative flair.
|
| 712 |
+
Draw connections to literature, art, and the human experience.
|
| 713 |
+
Use rich imagery and evocative language in your responses.
|
| 714 |
+
Even when explaining factual information, find ways to make your language sing.`,
|
| 715 |
+
temperature: 0.9,
|
| 716 |
+
emoji: "\u{1F3AD}"
|
| 717 |
+
},
|
| 718 |
+
concise: {
|
| 719 |
+
name: "Concise",
|
| 720 |
+
description: "Brief and to-the-point with no unnecessary words.",
|
| 721 |
+
systemPrompt: `You are a concise AI assistant that values brevity and clarity.
|
| 722 |
+
Provide the shortest possible response that fully answers the query.
|
| 723 |
+
Use bullet points where appropriate.
|
| 724 |
+
Eliminate unnecessary words, phrases, and preambles.
|
| 725 |
+
Focus only on the most essential information.`,
|
| 726 |
+
temperature: 0.5,
|
| 727 |
+
emoji: "\u{1F4CB}"
|
| 728 |
+
}
|
| 729 |
+
};
|
| 730 |
+
function getPersonalityConfig(personality) {
|
| 731 |
+
return personalityConfigs[personality] || personalityConfigs.default;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
// server/flux.ts
|
| 735 |
+
import { z as z2 } from "zod";
|
| 736 |
+
var imageGenerationSchema = z2.object({
|
| 737 |
+
prompt: z2.string().min(1).max(1e3),
|
| 738 |
+
seed: z2.number().optional().default(0),
|
| 739 |
+
randomize_seed: z2.boolean().optional().default(true),
|
| 740 |
+
width: z2.number().min(256).max(1024).optional().default(512),
|
| 741 |
+
height: z2.number().min(256).max(1024).optional().default(512),
|
| 742 |
+
guidance_scale: z2.number().min(0).max(20).optional().default(7.5),
|
| 743 |
+
num_inference_steps: z2.number().min(1).max(50).optional().default(20)
|
| 744 |
+
});
|
| 745 |
+
async function generateImage(params) {
|
| 746 |
+
try {
|
| 747 |
+
const apiKey = process.env.REPLICATE_API_KEY;
|
| 748 |
+
if (!apiKey) {
|
| 749 |
+
throw new Error("REPLICATE_API_KEY is not set in environment variables");
|
| 750 |
+
}
|
| 751 |
+
const inputData = {
|
| 752 |
+
input: {
|
| 753 |
+
prompt: params.prompt,
|
| 754 |
+
width: params.width,
|
| 755 |
+
height: params.height,
|
| 756 |
+
seed: params.randomize_seed ? Math.floor(Math.random() * 1e6) : params.seed,
|
| 757 |
+
guidance_scale: params.guidance_scale,
|
| 758 |
+
num_inference_steps: params.num_inference_steps
|
| 759 |
+
}
|
| 760 |
+
};
|
| 761 |
+
const startResponse = await fetch(
|
| 762 |
+
"https://api.replicate.com/v1/models/black-forest-labs/flux-dev/predictions",
|
| 763 |
+
{
|
| 764 |
+
method: "POST",
|
| 765 |
+
headers: {
|
| 766 |
+
"Authorization": `Bearer ${apiKey}`,
|
| 767 |
+
"Content-Type": "application/json"
|
| 768 |
+
},
|
| 769 |
+
body: JSON.stringify(inputData)
|
| 770 |
+
}
|
| 771 |
+
);
|
| 772 |
+
if (!startResponse.ok) {
|
| 773 |
+
const errorData = await startResponse.json();
|
| 774 |
+
throw new Error(`Replicate API error: ${JSON.stringify(errorData)}`);
|
| 775 |
+
}
|
| 776 |
+
const prediction = await startResponse.json();
|
| 777 |
+
const predictionId = prediction.id;
|
| 778 |
+
let imageUrl = null;
|
| 779 |
+
let attempts = 0;
|
| 780 |
+
const maxAttempts = 30;
|
| 781 |
+
while (!imageUrl && attempts < maxAttempts) {
|
| 782 |
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
| 783 |
+
const statusResponse = await fetch(
|
| 784 |
+
`https://api.replicate.com/v1/predictions/${predictionId}`,
|
| 785 |
+
{
|
| 786 |
+
headers: {
|
| 787 |
+
"Authorization": `Bearer ${apiKey}`
|
| 788 |
+
}
|
| 789 |
+
}
|
| 790 |
+
);
|
| 791 |
+
if (!statusResponse.ok) {
|
| 792 |
+
const errorData = await statusResponse.json();
|
| 793 |
+
throw new Error(`Replicate API status error: ${JSON.stringify(errorData)}`);
|
| 794 |
+
}
|
| 795 |
+
const status = await statusResponse.json();
|
| 796 |
+
if (status.status === "succeeded") {
|
| 797 |
+
if (status.output && typeof status.output === "string") {
|
| 798 |
+
imageUrl = status.output;
|
| 799 |
+
} else if (Array.isArray(status.output) && status.output.length > 0) {
|
| 800 |
+
imageUrl = status.output[0];
|
| 801 |
+
}
|
| 802 |
+
} else if (status.status === "failed") {
|
| 803 |
+
throw new Error(`Image generation failed: ${status.error || "Unknown error"}`);
|
| 804 |
+
}
|
| 805 |
+
attempts++;
|
| 806 |
+
}
|
| 807 |
+
if (!imageUrl) {
|
| 808 |
+
throw new Error("Timed out waiting for image generation");
|
| 809 |
+
}
|
| 810 |
+
return imageUrl;
|
| 811 |
+
} catch (error) {
|
| 812 |
+
console.error("Error generating image:", error);
|
| 813 |
+
throw new Error(`Failed to generate image: ${error instanceof Error ? error.message : String(error)}`);
|
| 814 |
+
}
|
| 815 |
+
}
|
| 816 |
+
async function isFluxAvailable() {
|
| 817 |
+
try {
|
| 818 |
+
const apiKey = process.env.REPLICATE_API_KEY;
|
| 819 |
+
if (!apiKey) {
|
| 820 |
+
return false;
|
| 821 |
+
}
|
| 822 |
+
const response = await fetch(
|
| 823 |
+
"https://api.replicate.com/v1/models/black-forest-labs/flux-dev",
|
| 824 |
+
{
|
| 825 |
+
headers: {
|
| 826 |
+
"Authorization": `Bearer ${apiKey}`
|
| 827 |
+
}
|
| 828 |
+
}
|
| 829 |
+
);
|
| 830 |
+
return response.ok;
|
| 831 |
+
} catch (error) {
|
| 832 |
+
console.error("Error checking FLUX availability:", error);
|
| 833 |
+
return false;
|
| 834 |
+
}
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
// server/video.ts
|
| 838 |
+
import { InferenceClient as InferenceClient2 } from "@huggingface/inference";
|
| 839 |
+
import { z as z3 } from "zod";
|
| 840 |
+
var videoGenerationSchema = z3.object({
|
| 841 |
+
prompt: z3.string().min(1).max(1e3),
|
| 842 |
+
model: z3.enum(["Wan-AI/Wan2.1-T2V-14B"]).default("Wan-AI/Wan2.1-T2V-14B")
|
| 843 |
+
});
|
| 844 |
+
async function generateVideo(params) {
|
| 845 |
+
try {
|
| 846 |
+
const replicateApiKey = process.env.REPLICATE_API_KEY;
|
| 847 |
+
if (!replicateApiKey) {
|
| 848 |
+
throw new Error("REPLICATE_API_KEY is not set in environment variables");
|
| 849 |
+
}
|
| 850 |
+
const client = new InferenceClient2(replicateApiKey);
|
| 851 |
+
const result = await client.textToVideo({
|
| 852 |
+
provider: "replicate",
|
| 853 |
+
model: params.model,
|
| 854 |
+
inputs: params.prompt
|
| 855 |
+
});
|
| 856 |
+
if (!result) {
|
| 857 |
+
throw new Error("Failed to generate video: No result returned");
|
| 858 |
+
}
|
| 859 |
+
const videoBuffer = await result.arrayBuffer();
|
| 860 |
+
const videoBase64 = Buffer.from(videoBuffer).toString("base64");
|
| 861 |
+
const videoUrl = `data:video/mp4;base64,${videoBase64}`;
|
| 862 |
+
return videoUrl;
|
| 863 |
+
} catch (error) {
|
| 864 |
+
console.error("Error generating video:", error);
|
| 865 |
+
throw new Error(`Failed to generate video: ${error instanceof Error ? error.message : String(error)}`);
|
| 866 |
+
}
|
| 867 |
+
}
|
| 868 |
+
async function isVideoGenerationAvailable() {
|
| 869 |
+
try {
|
| 870 |
+
const replicateApiKey = process.env.REPLICATE_API_KEY;
|
| 871 |
+
if (!replicateApiKey) {
|
| 872 |
+
return false;
|
| 873 |
+
}
|
| 874 |
+
const client = new InferenceClient2(replicateApiKey);
|
| 875 |
+
return !!client;
|
| 876 |
+
} catch (error) {
|
| 877 |
+
console.error("Error checking video generation availability:", error);
|
| 878 |
+
return false;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
// server/routes.ts
|
| 883 |
+
import OpenAI2 from "openai";
|
| 884 |
+
import { nanoid as nanoid2 } from "nanoid";
|
| 885 |
+
var currentModelStatus = {
|
| 886 |
+
model: "openai",
|
| 887 |
+
isOpenAIAvailable: true,
|
| 888 |
+
isQwenAvailable: true,
|
| 889 |
+
lastChecked: /* @__PURE__ */ new Date()
|
| 890 |
+
};
|
| 891 |
+
async function updateModelStatus() {
|
| 892 |
+
try {
|
| 893 |
+
const isOpenAIAvailable = await canUseOpenAI();
|
| 894 |
+
const isQwenAvailable = await canUseQwen();
|
| 895 |
+
let model = "unavailable";
|
| 896 |
+
if (isOpenAIAvailable) {
|
| 897 |
+
model = "openai";
|
| 898 |
+
} else if (isQwenAvailable) {
|
| 899 |
+
model = "qwen";
|
| 900 |
+
}
|
| 901 |
+
currentModelStatus = {
|
| 902 |
+
model,
|
| 903 |
+
isOpenAIAvailable,
|
| 904 |
+
isQwenAvailable,
|
| 905 |
+
lastChecked: /* @__PURE__ */ new Date()
|
| 906 |
+
};
|
| 907 |
+
console.log(`Updated model status: ${model} (OpenAI: ${isOpenAIAvailable}, Qwen: ${isQwenAvailable})`);
|
| 908 |
+
return currentModelStatus;
|
| 909 |
+
} catch (error) {
|
| 910 |
+
console.error("Error updating model status:", error);
|
| 911 |
+
return currentModelStatus;
|
| 912 |
+
}
|
| 913 |
+
}
|
| 914 |
+
updateModelStatus();
|
| 915 |
+
async function registerRoutes(app2) {
|
| 916 |
+
setupAuth(app2);
|
| 917 |
+
app2.get("/api/conversations", async (req, res) => {
|
| 918 |
+
try {
|
| 919 |
+
let conversations2;
|
| 920 |
+
if (req.isAuthenticated() && req.user) {
|
| 921 |
+
const userId = req.user.id;
|
| 922 |
+
conversations2 = await storage.getUserConversations(userId);
|
| 923 |
+
} else {
|
| 924 |
+
conversations2 = await storage.getConversations();
|
| 925 |
+
conversations2 = conversations2.filter((conv) => !conv.userId);
|
| 926 |
+
}
|
| 927 |
+
res.json(conversations2);
|
| 928 |
+
} catch (error) {
|
| 929 |
+
console.error("Error fetching conversations:", error);
|
| 930 |
+
res.status(500).json({ message: "Failed to fetch conversations." });
|
| 931 |
+
}
|
| 932 |
+
});
|
| 933 |
+
app2.post("/api/conversations", async (req, res) => {
|
| 934 |
+
try {
|
| 935 |
+
const conversationId = nanoid2();
|
| 936 |
+
let title = req.body.title;
|
| 937 |
+
if ((!title || title === "New Conversation") && req.body.firstMessage) {
|
| 938 |
+
try {
|
| 939 |
+
const openaiClient = new OpenAI2();
|
| 940 |
+
const response = await openaiClient.chat.completions.create({
|
| 941 |
+
model: "gpt-3.5-turbo",
|
| 942 |
+
messages: [\
|
| 943 |
+
{\
|
| 944 |
+
role: "system",\
|
| 945 |
+
content: "Generate a brief, descriptive title (3-5 words) for a conversation that starts with this message. Respond with just the title."\
|
| 946 |
+
},\
|
| 947 |
+
{\
|
| 948 |
+
role: "user",\
|
| 949 |
+
content: req.body.firstMessage\
|
| 950 |
+
}\
|
| 951 |
+
],
|
| 952 |
+
max_tokens: 20,
|
| 953 |
+
temperature: 0.7
|
| 954 |
+
});
|
| 955 |
+
title = response.choices[0].message.content?.trim() || "New Conversation";
|
| 956 |
+
} catch (err) {
|
| 957 |
+
console.error("Error generating AI title:", err);
|
| 958 |
+
title = "New Conversation";
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
const conversationData = {
|
| 962 |
+
id: conversationId,
|
| 963 |
+
title,
|
| 964 |
+
personality: req.body.personality || "general"
|
| 965 |
+
};
|
| 966 |
+
if (req.isAuthenticated() && req.user) {
|
| 967 |
+
conversationData.userId = req.user.id;
|
| 968 |
+
}
|
| 969 |
+
const result = insertConversationSchema.safeParse(conversationData);
|
| 970 |
+
if (!result.success) {
|
| 971 |
+
return res.status(400).json({ message: "Invalid conversation data." });
|
| 972 |
+
}
|
| 973 |
+
const conversation = await storage.createConversation(result.data);
|
| 974 |
+
res.status(201).json(conversation);
|
| 975 |
+
} catch (error) {
|
| 976 |
+
console.error("Error creating conversation:", error);
|
| 977 |
+
res.status(500).json({ message: "Failed to create conversation." });
|
| 978 |
+
}
|
| 979 |
+
});
|
| 980 |
+
app2.post("/api/conversations/:id/generate-title", async (req, res) => {
|
| 981 |
+
try {
|
| 982 |
+
const { id } = req.params;
|
| 983 |
+
const messages2 = await storage.getMessages(id);
|
| 984 |
+
if (messages2.length < 2) {
|
| 985 |
+
return res.status(400).json({ message: "Need at least one exchange to generate a title" });
|
| 986 |
+
}
|
| 987 |
+
const contextMessages = messages2.slice(0, Math.min(4, messages2.length)).map((msg) => `${msg.role}: ${msg.content}`).join("\n");
|
| 988 |
+
let title;
|
| 989 |
+
try {
|
| 990 |
+
const openaiClient = new OpenAI2();
|
| 991 |
+
const response = await openaiClient.chat.completions.create({
|
| 992 |
+
model: "gpt-3.5-turbo",
|
| 993 |
+
messages: [\
|
| 994 |
+
{\
|
| 995 |
+
role: "system",\
|
| 996 |
+
content: "You are a helpful assistant that generates short, descriptive titles (max 6 words) for conversations based on their content. Respond with just the title."\
|
| 997 |
+
},\
|
| 998 |
+
{\
|
| 999 |
+
role: "user",\
|
| 1000 |
+
content: `Generate a short, descriptive title (maximum 6 words) for this conversation:\
|
| 1001 |
+
${contextMessages}`\
|
| 1002 |
+
}\
|
| 1003 |
+
],
|
| 1004 |
+
max_tokens: 20,
|
| 1005 |
+
temperature: 0.7
|
| 1006 |
+
});
|
| 1007 |
+
title = response.choices[0].message.content?.trim();
|
| 1008 |
+
if (!title) {
|
| 1009 |
+
title = `Chat ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`;
|
| 1010 |
+
}
|
| 1011 |
+
} catch (err) {
|
| 1012 |
+
console.error("Error generating AI title:", err);
|
| 1013 |
+
title = `Chat ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`;
|
| 1014 |
+
}
|
| 1015 |
+
const updatedConversation = await storage.updateConversationTitle(id, title);
|
| 1016 |
+
if (!updatedConversation) {
|
| 1017 |
+
return res.status(404).json({ message: "Conversation not found" });
|
| 1018 |
+
}
|
| 1019 |
+
res.json(updatedConversation);
|
| 1020 |
+
} catch (error) {
|
| 1021 |
+
console.error("Error generating title:", error);
|
| 1022 |
+
res.status(500).json({ message: "Failed to generate title." });
|
| 1023 |
+
}
|
| 1024 |
+
});
|
| 1025 |
+
app2.get("/api/conversations/:id/messages", async (req, res) => {
|
| 1026 |
+
try {
|
| 1027 |
+
const { id } = req.params;
|
| 1028 |
+
const conversation = await storage.getConversation(id);
|
| 1029 |
+
if (!conversation) {
|
| 1030 |
+
return res.status(404).json({ message: "Conversation not found." });
|
| 1031 |
+
}
|
| 1032 |
+
if (conversation.userId && req.isAuthenticated() && req.user) {
|
| 1033 |
+
if (conversation.userId !== req.user.id) {
|
| 1034 |
+
return res.status(403).json({ message: "You don't have permission to access this conversation." });
|
| 1035 |
+
}
|
| 1036 |
+
}
|
| 1037 |
+
const messages2 = await storage.getMessages(id);
|
| 1038 |
+
if (req.isAuthenticated() && req.user) {
|
| 1039 |
+
const userContext = {
|
| 1040 |
+
role: "system",
|
| 1041 |
+
content: req.user.systemContext || `Chat with ${req.user.username}`,
|
| 1042 |
+
conversationId: id,
|
| 1043 |
+
createdAt: /* @__PURE__ */ new Date()
|
| 1044 |
+
};
|
| 1045 |
+
messages2.unshift(userContext);
|
| 1046 |
+
}
|
| 1047 |
+
res.json(messages2);
|
| 1048 |
+
} catch (error) {
|
| 1049 |
+
console.error("Error fetching messages:", error);
|
| 1050 |
+
res.status(500).json({ message: "Failed to fetch messages." });
|
| 1051 |
+
}
|
| 1052 |
+
});
|
| 1053 |
+
app2.post("/api/chat", async (req, res) => {
|
| 1054 |
+
try {
|
| 1055 |
+
await updateModelStatus();
|
| 1056 |
+
if (currentModelStatus.model === "unavailable") {
|
| 1057 |
+
return res.status(503).json({
|
| 1058 |
+
message: "All AI models are currently unavailable. Please check your API keys."
|
| 1059 |
+
});
|
| 1060 |
+
}
|
| 1061 |
+
const result = conversationSchema.safeParse(req.body);
|
| 1062 |
+
if (!result.success) {
|
| 1063 |
+
return res.status(400).json({ message: "Invalid chat data format." });
|
| 1064 |
+
}
|
| 1065 |
+
const { messages: messages2 } = result.data;
|
| 1066 |
+
const conversationId = req.body.conversationId || "default";
|
| 1067 |
+
const conversation = await storage.getConversation(conversationId);
|
| 1068 |
+
if (!conversation && conversationId !== "default") {
|
| 1069 |
+
return res.status(404).json({ message: "Conversation not found." });
|
| 1070 |
+
}
|
| 1071 |
+
if (conversation && conversation.userId) {
|
| 1072 |
+
if (!req.isAuthenticated() || !req.user || conversation.userId !== req.user.id) {
|
| 1073 |
+
return res.status(403).json({ message: "You don't have permission to access this conversation." });
|
| 1074 |
+
}
|
| 1075 |
+
}
|
| 1076 |
+
const userMessage = messages2[messages2.length - 1];
|
| 1077 |
+
if (userMessage.role !== "user") {
|
| 1078 |
+
return res.status(400).json({ message: "Last message must be from the user." });
|
| 1079 |
+
}
|
| 1080 |
+
await storage.createMessage({
|
| 1081 |
+
content: userMessage.content,
|
| 1082 |
+
role: userMessage.role,
|
| 1083 |
+
conversationId
|
| 1084 |
+
});
|
| 1085 |
+
let userSystemContext = void 0;
|
| 1086 |
+
if (req.isAuthenticated() && req.user && req.user.systemContext) {
|
| 1087 |
+
userSystemContext = req.user.systemContext;
|
| 1088 |
+
console.log(
|
| 1089 |
+
"Including user system context in conversation:",
|
| 1090 |
+
userSystemContext ? "Yes" : "None available"
|
| 1091 |
+
);
|
| 1092 |
+
}
|
| 1093 |
+
const aiResponse = await generateChatResponse(messages2, userSystemContext);
|
| 1094 |
+
const savedMessage = await storage.createMessage({
|
| 1095 |
+
content: aiResponse,
|
| 1096 |
+
role: "assistant",
|
| 1097 |
+
conversationId
|
| 1098 |
+
});
|
| 1099 |
+
res.json({
|
| 1100 |
+
message: savedMessage,
|
| 1101 |
+
conversationId,
|
| 1102 |
+
modelInfo: {
|
| 1103 |
+
model: currentModelStatus.model,
|
| 1104 |
+
isFallback: currentModelStatus.model !== "openai"
|
| 1105 |
+
}
|
| 1106 |
+
});
|
| 1107 |
+
} catch (error) {
|
| 1108 |
+
console.error("Chat API error:", error);
|
| 1109 |
+
res.status(500).json({
|
| 1110 |
+
message: error.message || "Failed to process chat message."
|
| 1111 |
+
});
|
| 1112 |
+
}
|
| 1113 |
+
});
|
| 1114 |
+
app2.get("/api/model-status", async (_req, res) => {
|
| 1115 |
+
try {
|
| 1116 |
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1e3);
|
| 1117 |
+
if (currentModelStatus.lastChecked < fiveMinutesAgo) {
|
| 1118 |
+
await updateModelStatus();
|
| 1119 |
+
}
|
| 1120 |
+
return res.json(currentModelStatus);
|
| 1121 |
+
} catch (error) {
|
| 1122 |
+
console.error("Error getting model status:", error);
|
| 1123 |
+
return res.status(500).json({ message: "Failed to get model status" });
|
| 1124 |
+
}
|
| 1125 |
+
});
|
| 1126 |
+
app2.delete("/api/conversations/:id", async (req, res) => {
|
| 1127 |
+
try {
|
| 1128 |
+
const { id } = req.params;
|
| 1129 |
+
if (id === "default") {
|
| 1130 |
+
return res.status(400).json({ message: "Cannot delete the default conversation" });
|
| 1131 |
+
}
|
| 1132 |
+
const conversation = await storage.getConversation(id);
|
| 1133 |
+
if (!conversation) {
|
| 1134 |
+
return res.status(404).json({ message: "Conversation not found" });
|
| 1135 |
+
}
|
| 1136 |
+
if (conversation.userId && req.isAuthenticated() && req.user) {
|
| 1137 |
+
if (conversation.userId !== req.user.id) {
|
| 1138 |
+
return res.status(403).json({ message: "You don't have permission to delete this conversation." });
|
| 1139 |
+
}
|
| 1140 |
+
}
|
| 1141 |
+
const success = await storage.deleteConversation(id);
|
| 1142 |
+
if (success) {
|
| 1143 |
+
res.status(200).json({ message: "Conversation deleted successfully" });
|
| 1144 |
+
} else {
|
| 1145 |
+
res.status(500).json({ message: "Failed to delete conversation" });
|
| 1146 |
+
}
|
| 1147 |
+
} catch (error) {
|
| 1148 |
+
console.error("Error deleting conversation:", error);
|
| 1149 |
+
res.status(500).json({ message: "Server error deleting conversation" });
|
| 1150 |
+
}
|
| 1151 |
+
});
|
| 1152 |
+
app2.patch("/api/conversations/:id/title", async (req, res) => {
|
| 1153 |
+
try {
|
| 1154 |
+
const { id } = req.params;
|
| 1155 |
+
const { title } = req.body;
|
| 1156 |
+
if (!title || typeof title !== "string" || title.trim().length === 0) {
|
| 1157 |
+
return res.status(400).json({ message: "Valid title is required" });
|
| 1158 |
+
}
|
| 1159 |
+
const conversation = await storage.getConversation(id);
|
| 1160 |
+
if (!conversation) {
|
| 1161 |
+
return res.status(404).json({ message: "Conversation not found" });
|
| 1162 |
+
}
|
| 1163 |
+
if (conversation.userId && req.isAuthenticated() && req.user) {
|
| 1164 |
+
if (conversation.userId !== req.user.id) {
|
| 1165 |
+
return res.status(403).json({ message: "You don't have permission to update this conversation." });
|
| 1166 |
+
}
|
| 1167 |
+
}
|
| 1168 |
+
const updatedConversation = await storage.createConversation({
|
| 1169 |
+
...conversation,
|
| 1170 |
+
title: title.trim()
|
| 1171 |
+
});
|
| 1172 |
+
res.json(updatedConversation);
|
| 1173 |
+
} catch (error) {
|
| 1174 |
+
console.error("Error updating conversation title:", error);
|
| 1175 |
+
res.status(500).json({ message: "Failed to update conversation title" });
|
| 1176 |
+
}
|
| 1177 |
+
});
|
| 1178 |
+
app2.patch("/api/conversations/:id/personality", async (req, res) => {
|
| 1179 |
+
try {
|
| 1180 |
+
const { id } = req.params;
|
| 1181 |
+
const { personality } = req.body;
|
| 1182 |
+
const result = personalityTypeSchema.safeParse(personality);
|
| 1183 |
+
if (!result.success) {
|
| 1184 |
+
return res.status(400).json({
|
| 1185 |
+
message: "Invalid personality type",
|
| 1186 |
+
validOptions: personalityTypeSchema.options
|
| 1187 |
+
});
|
| 1188 |
+
}
|
| 1189 |
+
const conversation = await storage.getConversation(id);
|
| 1190 |
+
if (!conversation) {
|
| 1191 |
+
return res.status(404).json({ message: "Conversation not found" });
|
| 1192 |
+
}
|
| 1193 |
+
if (conversation.userId && req.isAuthenticated() && req.user) {
|
| 1194 |
+
if (conversation.userId !== req.user.id) {
|
| 1195 |
+
return res.status(403).json({ message: "You don't have permission to update this conversation." });
|
| 1196 |
+
}
|
| 1197 |
+
}
|
| 1198 |
+
const updatedConversation = await storage.updateConversationPersonality(id, result.data);
|
| 1199 |
+
const personalityConfig = getPersonalityConfig(result.data);
|
| 1200 |
+
res.json({
|
| 1201 |
+
...updatedConversation,
|
| 1202 |
+
personalityConfig: {
|
| 1203 |
+
name: personalityConfig.name,
|
| 1204 |
+
description: personalityConfig.description,
|
| 1205 |
+
emoji: personalityConfig.emoji
|
| 1206 |
+
}
|
| 1207 |
+
});
|
| 1208 |
+
} catch (error) {
|
| 1209 |
+
console.error("Error updating conversation personality:", error);
|
| 1210 |
+
res.status(500).json({ message: "Failed to update conversation personality" });
|
| 1211 |
+
}
|
| 1212 |
+
});
|
| 1213 |
+
app2.get("/api/personalities", async (_req, res) => {
|
| 1214 |
+
try {
|
| 1215 |
+
const personalityTypes = personalityTypeSchema.options;
|
| 1216 |
+
const personalities = personalityTypes.map((type) => {
|
| 1217 |
+
const config = getPersonalityConfig(type);
|
| 1218 |
+
return {
|
| 1219 |
+
id: type,
|
| 1220 |
+
name: config.name,
|
| 1221 |
+
description: config.description,
|
| 1222 |
+
emoji: config.emoji
|
| 1223 |
+
};
|
| 1224 |
+
});
|
| 1225 |
+
res.json(personalities);
|
| 1226 |
+
} catch (error) {
|
| 1227 |
+
console.error("Error fetching personalities:", error);
|
| 1228 |
+
res.status(500).json({ message: "Failed to fetch personalities" });
|
| 1229 |
+
}
|
| 1230 |
+
});
|
| 1231 |
+
app2.post("/api/generate-image", async (req, res) => {
|
| 1232 |
+
try {
|
| 1233 |
+
const result = imageGenerationSchema.safeParse(req.body);
|
| 1234 |
+
if (!result.success) {
|
| 1235 |
+
return res.status(400).json({
|
| 1236 |
+
message: "Invalid image generation parameters",
|
| 1237 |
+
errors: result.error.format()
|
| 1238 |
+
});
|
| 1239 |
+
}
|
| 1240 |
+
const imageUrl = await generateImage(result.data);
|
| 1241 |
+
return res.json({
|
| 1242 |
+
success: true,
|
| 1243 |
+
imageUrl,
|
| 1244 |
+
params: result.data
|
| 1245 |
+
});
|
| 1246 |
+
} catch (error) {
|
| 1247 |
+
console.error("Error generating image:", error);
|
| 1248 |
+
return res.status(500).json({
|
| 1249 |
+
success: false,
|
| 1250 |
+
message: error.message || "Failed to generate image"
|
| 1251 |
+
});
|
| 1252 |
+
}
|
| 1253 |
+
});
|
| 1254 |
+
app2.get("/api/flux-status", async (_req, res) => {
|
| 1255 |
+
try {
|
| 1256 |
+
const isAvailable = await isFluxAvailable();
|
| 1257 |
+
return res.json({
|
| 1258 |
+
isAvailable,
|
| 1259 |
+
model: "FLUX.1-dev"
|
| 1260 |
+
});
|
| 1261 |
+
} catch (error) {
|
| 1262 |
+
console.error("Error checking FLUX availability:", error);
|
| 1263 |
+
return res.status(500).json({
|
| 1264 |
+
isAvailable: false,
|
| 1265 |
+
message: "Error checking FLUX availability"
|
| 1266 |
+
});
|
| 1267 |
+
}
|
| 1268 |
+
});
|
| 1269 |
+
app2.post("/api/generate-video", async (req, res) => {
|
| 1270 |
+
try {
|
| 1271 |
+
const result = videoGenerationSchema.safeParse(req.body);
|
| 1272 |
+
if (!result.success) {
|
| 1273 |
+
return res.status(400).json({
|
| 1274 |
+
message: "Invalid video generation parameters",
|
| 1275 |
+
errors: result.error.format()
|
| 1276 |
+
});
|
| 1277 |
+
}
|
| 1278 |
+
const videoUrl = await generateVideo(result.data);
|
| 1279 |
+
return res.json({
|
| 1280 |
+
success: true,
|
| 1281 |
+
videoUrl,
|
| 1282 |
+
params: result.data
|
| 1283 |
+
});
|
| 1284 |
+
} catch (error) {
|
| 1285 |
+
console.error("Error generating video:", error);
|
| 1286 |
+
return res.status(500).json({
|
| 1287 |
+
success: false,
|
| 1288 |
+
message: error.message || "Failed to generate video"
|
| 1289 |
+
});
|
| 1290 |
+
}
|
| 1291 |
+
});
|
| 1292 |
+
app2.get("/api/video-status", async (_req, res) => {
|
| 1293 |
+
try {
|
| 1294 |
+
const isAvailable = await isVideoGenerationAvailable();
|
| 1295 |
+
return res.json({
|
| 1296 |
+
isAvailable,
|
| 1297 |
+
model: "Wan-AI/Wan2.1-T2V-14B"
|
| 1298 |
+
});
|
| 1299 |
+
} catch (error) {
|
| 1300 |
+
console.error("Error checking video generation availability:", error);
|
| 1301 |
+
return res.status(500).json({
|
| 1302 |
+
isAvailable: false,
|
| 1303 |
+
message: "Error checking video generation availability"
|
| 1304 |
+
});
|
| 1305 |
+
}
|
| 1306 |
+
});
|
| 1307 |
+
app2.get("/api/health", (_req, res) => {
|
| 1308 |
+
return res.json({ status: "ok" });
|
| 1309 |
+
});
|
| 1310 |
+
const httpServer = createServer(app2);
|
| 1311 |
+
return httpServer;
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
// server/vite.ts
|
| 1315 |
+
import express from "express";
|
| 1316 |
+
import fs from "fs";
|
| 1317 |
+
import path2 from "path";
|
| 1318 |
+
import { createServer as createViteServer, createLogger } from "vite";
|
| 1319 |
+
|
| 1320 |
+
// vite.config.ts
|
| 1321 |
+
import { defineConfig } from "vite";
|
| 1322 |
+
import react from "@vitejs/plugin-react";
|
| 1323 |
+
import path from "path";
|
| 1324 |
+
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
| 1325 |
+
var vite_config_default = defineConfig({
|
| 1326 |
+
plugins: [\
|
| 1327 |
+
react(),\
|
| 1328 |
+
runtimeErrorOverlay(),\
|
| 1329 |
+
...process.env.NODE_ENV !== "production" && process.env.REPL_ID !== void 0 ? [\
|
| 1330 |
+
await import("@replit/vite-plugin-cartographer").then(\
|
| 1331 |
+
(m) => m.cartographer()\
|
| 1332 |
+
)\
|
| 1333 |
+
] : []\
|
| 1334 |
+
],
|
| 1335 |
+
resolve: {
|
| 1336 |
+
alias: {
|
| 1337 |
+
"@": path.resolve(import.meta.dirname, "client", "src"),
|
| 1338 |
+
"@shared": path.resolve(import.meta.dirname, "shared"),
|
| 1339 |
+
"@assets": path.resolve(import.meta.dirname, "attached_assets")
|
| 1340 |
+
}
|
| 1341 |
+
},
|
| 1342 |
+
root: path.resolve(import.meta.dirname, "client"),
|
| 1343 |
+
build: {
|
| 1344 |
+
outDir: path.resolve(import.meta.dirname, "dist/public"),
|
| 1345 |
+
emptyOutDir: true
|
| 1346 |
+
}
|
| 1347 |
+
});
|
| 1348 |
+
|
| 1349 |
+
// server/vite.ts
|
| 1350 |
+
import { nanoid as nanoid3 } from "nanoid";
|
| 1351 |
+
var viteLogger = createLogger();
|
| 1352 |
+
function log(message, source = "express") {
|
| 1353 |
+
const formattedTime = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
|
| 1354 |
+
hour: "numeric",
|
| 1355 |
+
minute: "2-digit",
|
| 1356 |
+
second: "2-digit",
|
| 1357 |
+
hour12: true
|
| 1358 |
+
});
|
| 1359 |
+
console.log(`${formattedTime} [${source}] ${message}`);
|
| 1360 |
+
}
|
| 1361 |
+
async function setupVite(app2, server) {
|
| 1362 |
+
const serverOptions = {
|
| 1363 |
+
middlewareMode: true,
|
| 1364 |
+
hmr: { server },
|
| 1365 |
+
allowedHosts: true
|
| 1366 |
+
};
|
| 1367 |
+
const vite = await createViteServer({
|
| 1368 |
+
...vite_config_default,
|
| 1369 |
+
configFile: false,
|
| 1370 |
+
customLogger: {
|
| 1371 |
+
...viteLogger,
|
| 1372 |
+
error: (msg, options) => {
|
| 1373 |
+
viteLogger.error(msg, options);
|
| 1374 |
+
process.exit(1);
|
| 1375 |
+
}
|
| 1376 |
+
},
|
| 1377 |
+
server: serverOptions,
|
| 1378 |
+
appType: "custom"
|
| 1379 |
+
});
|
| 1380 |
+
app2.use(vite.middlewares);
|
| 1381 |
+
app2.use("*", async (req, res, next) => {
|
| 1382 |
+
const url = req.originalUrl;
|
| 1383 |
+
try {
|
| 1384 |
+
const clientTemplate = path2.resolve(
|
| 1385 |
+
import.meta.dirname,
|
| 1386 |
+
"..",
|
| 1387 |
+
"client",
|
| 1388 |
+
"index.html"
|
| 1389 |
+
);
|
| 1390 |
+
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
| 1391 |
+
template = template.replace(
|
| 1392 |
+
`src="/src/main.tsx"`,
|
| 1393 |
+
`src="/src/main.tsx?v=${nanoid3()}"`
|
| 1394 |
+
);
|
| 1395 |
+
const page = await vite.transformIndexHtml(url, template);
|
| 1396 |
+
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
| 1397 |
+
} catch (e) {
|
| 1398 |
+
vite.ssrFixStacktrace(e);
|
| 1399 |
+
next(e);
|
| 1400 |
+
}
|
| 1401 |
+
});
|
| 1402 |
+
}
|
| 1403 |
+
function serveStatic(app2) {
|
| 1404 |
+
const distPath = path2.resolve(import.meta.dirname, "public");
|
| 1405 |
+
if (!fs.existsSync(distPath)) {
|
| 1406 |
+
throw new Error(
|
| 1407 |
+
`Could not find the build directory: ${distPath}, make sure to build the client first`
|
| 1408 |
+
);
|
| 1409 |
+
}
|
| 1410 |
+
app2.use(express.static(distPath));
|
| 1411 |
+
app2.use("*", (_req, res) => {
|
| 1412 |
+
res.sendFile(path2.resolve(distPath, "index.html"));
|
| 1413 |
+
});
|
| 1414 |
+
}
|
| 1415 |
+
|
| 1416 |
+
// server/index.ts
|
| 1417 |
+
var app = express2();
|
| 1418 |
+
app.use(express2.json());
|
| 1419 |
+
app.use(express2.urlencoded({ extended: false }));
|
| 1420 |
+
app.use((req, res, next) => {
|
| 1421 |
+
const start = Date.now();
|
| 1422 |
+
const path3 = req.path;
|
| 1423 |
+
let capturedJsonResponse = void 0;
|
| 1424 |
+
const originalResJson = res.json;
|
| 1425 |
+
res.json = function(bodyJson, ...args) {
|
| 1426 |
+
capturedJsonResponse = bodyJson;
|
| 1427 |
+
return originalResJson.apply(res, [bodyJson, ...args]);
|
| 1428 |
+
};
|
| 1429 |
+
res.on("finish", () => {
|
| 1430 |
+
const duration = Date.now() - start;
|
| 1431 |
+
if (path3.startsWith("/api")) {
|
| 1432 |
+
let logLine = `${req.method} ${path3} ${res.statusCode} in ${duration}ms`;
|
| 1433 |
+
if (capturedJsonResponse) {
|
| 1434 |
+
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
| 1435 |
+
}
|
| 1436 |
+
if (logLine.length > 80) {
|
| 1437 |
+
logLine = logLine.slice(0, 79) + "\u2026";
|
| 1438 |
+
}
|
| 1439 |
+
log(logLine);
|
| 1440 |
+
}
|
| 1441 |
+
});
|
| 1442 |
+
next();
|
| 1443 |
+
});
|
| 1444 |
+
(async () => {
|
| 1445 |
+
const server = await registerRoutes(app);
|
| 1446 |
+
app.use((err, _req, res, _next) => {
|
| 1447 |
+
const status = err.status || err.statusCode || 500;
|
| 1448 |
+
const message = err.message || "Internal Server Error";
|
| 1449 |
+
res.status(status).json({ message });
|
| 1450 |
+
throw err;
|
| 1451 |
+
});
|
| 1452 |
+
if (app.get("env") === "development") {
|
| 1453 |
+
await setupVite(app, server);
|
| 1454 |
+
} else {
|
| 1455 |
+
serveStatic(app);
|
| 1456 |
+
}
|
| 1457 |
+
const port = 5e3;
|
| 1458 |
+
server.listen({
|
| 1459 |
+
port,
|
| 1460 |
+
host: "0.0.0.0",
|
| 1461 |
+
reusePort: true
|
| 1462 |
+
}, () => {
|
| 1463 |
+
log(`serving on port ${port}`);
|
| 1464 |
+
});
|
| 1465 |
+
})();
|
| 1466 |
+
|
| 1467 |
+
```
|
OpenAIChatAssistant/attached_assets/content-1746448603342.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 404
|
| 2 |
+
|
| 3 |
+
**File not found**
|
| 4 |
+
|
| 5 |
+
The site configured at this address does not
|
| 6 |
+
contain the requested file.
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
If this is your site, make sure that the filename case matches the URL
|
| 10 |
+
as well as any file permissions.
|
| 11 |
+
|
| 12 |
+
For root URLs (like `http://example.com/`) you must provide an
|
| 13 |
+
`index.html` file.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
[Read the full documentation](https://help.github.com/pages/)
|
| 17 |
+
for more information about using **GitHub Pages**.
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
[GitHub Status](https://githubstatus.com/) —
|
| 21 |
+
[@githubstatus](https://twitter.com/githubstatus)
|
| 22 |
+
|
| 23 |
+
[](https://bella288.github.io/)[](https://bella288.github.io/)
|
OpenAIChatAssistant/attached_assets/screenshot-1746193704446.png
ADDED
|
OpenAIChatAssistant/attached_assets/screenshot-1746193776733.png
ADDED
|
OpenAIChatAssistant/client/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
| 6 |
+
</head>
|
| 7 |
+
<body>
|
| 8 |
+
<div id="root"></div>
|
| 9 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 10 |
+
<!-- This is a replit script which adds a banner on the top of the page when opened in development mode outside the replit environment -->
|
| 11 |
+
<script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
OpenAIChatAssistant/client/src/App.tsx
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Switch, Route } from "wouter";
|
| 2 |
+
import { queryClient } from "./lib/queryClient";
|
| 3 |
+
import { QueryClientProvider } from "@tanstack/react-query";
|
| 4 |
+
import { Toaster } from "@/components/ui/toaster";
|
| 5 |
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
| 6 |
+
import NotFound from "@/pages/not-found";
|
| 7 |
+
import Home from "@/pages/Home";
|
| 8 |
+
import AuthPage from "@/pages/auth-page";
|
| 9 |
+
import ImageGenPage from "@/pages/ImageGenPage";
|
| 10 |
+
import VideoGenPage from "@/pages/VideoGenPage";
|
| 11 |
+
import { AuthProvider } from "@/hooks/use-auth";
|
| 12 |
+
import { ProtectedRoute } from "@/lib/protected-route";
|
| 13 |
+
import LogoutPage from "@/pages/logout-page";
|
| 14 |
+
import RegisterPage from "@/pages/register-page";
|
| 15 |
+
|
| 16 |
+
function Router() {
|
| 17 |
+
return (
|
| 18 |
+
<Switch>
|
| 19 |
+
<Route path="/auth" component={AuthPage} />
|
| 20 |
+
<Route path="/register" component={RegisterPage} />
|
| 21 |
+
<Route path="/logout" component={LogoutPage} />
|
| 22 |
+
<ProtectedRoute path="/" component={Home} />
|
| 23 |
+
<ProtectedRoute path="/image-generator" component={ImageGenPage} />
|
| 24 |
+
<ProtectedRoute path="/video-generator" component={VideoGenPage} />
|
| 25 |
+
<Route component={NotFound} />
|
| 26 |
+
</Switch>
|
| 27 |
+
);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function App() {
|
| 31 |
+
return (
|
| 32 |
+
<QueryClientProvider client={queryClient}>
|
| 33 |
+
<AuthProvider>
|
| 34 |
+
<TooltipProvider>
|
| 35 |
+
<Toaster />
|
| 36 |
+
<Router />
|
| 37 |
+
</TooltipProvider>
|
| 38 |
+
</AuthProvider>
|
| 39 |
+
</QueryClientProvider>
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export default App;
|
OpenAIChatAssistant/client/src/components/ChatHistory.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { ChatHistoryProps } from '@/lib/types';
|
| 3 |
+
import TypingIndicator from './TypingIndicator';
|
| 4 |
+
import { useScrollToBottom } from '@/lib/hooks';
|
| 5 |
+
import { AlertTriangle } from 'lucide-react';
|
| 6 |
+
|
| 7 |
+
const ChatHistory: React.FC<ChatHistoryProps> = ({
|
| 8 |
+
messages,
|
| 9 |
+
isLoading,
|
| 10 |
+
currentModel = 'openai'
|
| 11 |
+
}) => {
|
| 12 |
+
const scrollRef = useScrollToBottom([messages, isLoading]);
|
| 13 |
+
|
| 14 |
+
// Check if we're in fallback mode by looking at the model or fallback indicator in messages
|
| 15 |
+
const isFallbackMode = currentModel === 'qwen' ||
|
| 16 |
+
messages.some(message =>
|
| 17 |
+
message.role === 'assistant' &&
|
| 18 |
+
message.content.includes('fallback mode')
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div
|
| 23 |
+
ref={scrollRef}
|
| 24 |
+
className="chat-container overflow-y-auto pb-4 px-2"
|
| 25 |
+
style={{ height: 'calc(100vh - 180px)' }}
|
| 26 |
+
>
|
| 27 |
+
{/* Fallback mode indicator */}
|
| 28 |
+
{isFallbackMode && (
|
| 29 |
+
<div className="bg-amber-50 border-l-4 border-amber-400 p-4 mb-4 rounded-md">
|
| 30 |
+
<div className="flex items-center">
|
| 31 |
+
<div className="flex-shrink-0">
|
| 32 |
+
<AlertTriangle className="h-5 w-5 text-amber-400" />
|
| 33 |
+
</div>
|
| 34 |
+
<div className="ml-3">
|
| 35 |
+
<p className="text-sm text-amber-700">
|
| 36 |
+
<strong>Qwen Fallback Mode Active:</strong> The OpenAI API is currently unavailable.
|
| 37 |
+
Responses are being generated by the Qwen model instead.
|
| 38 |
+
</p>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
)}
|
| 43 |
+
|
| 44 |
+
{messages.map((message, index) => {
|
| 45 |
+
// Check if this is a fallback message directly from the content
|
| 46 |
+
const isMessageFallback = message.role === 'assistant' &&
|
| 47 |
+
(message.content.includes('fallback mode') ||
|
| 48 |
+
message.content.includes('Qwen model'));
|
| 49 |
+
|
| 50 |
+
// Determine if this message appears to be a fallback response
|
| 51 |
+
const isAssistantFallbackMessage = message.role === 'assistant' &&
|
| 52 |
+
(currentModel === 'qwen' || isMessageFallback);
|
| 53 |
+
|
| 54 |
+
// Clean up fallback message for display
|
| 55 |
+
let displayContent = isAssistantFallbackMessage
|
| 56 |
+
? message.content.replace(/\n\n\(Note: I'm currently operating in fallback mode.*\)$/, '')
|
| 57 |
+
: message.content;
|
| 58 |
+
|
| 59 |
+
// Remove any thinking process sections for Qwen responses
|
| 60 |
+
if (isAssistantFallbackMessage) {
|
| 61 |
+
// Remove <think> tags and their content
|
| 62 |
+
displayContent = displayContent.replace(/<think>[\s\S]*?<\/think>/g, '');
|
| 63 |
+
|
| 64 |
+
// Remove any other XML-like tags
|
| 65 |
+
displayContent = displayContent.replace(/<[^>]*>/g, '');
|
| 66 |
+
|
| 67 |
+
// Clean up any excessive whitespace
|
| 68 |
+
displayContent = displayContent.replace(/^\s+|\s+$/g, '');
|
| 69 |
+
displayContent = displayContent.replace(/\n{3,}/g, '\n\n');
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return (
|
| 73 |
+
<div
|
| 74 |
+
key={index}
|
| 75 |
+
className={`flex items-start ${message.role === 'user' ? 'justify-end' : ''} mb-4`}
|
| 76 |
+
>
|
| 77 |
+
{message.role !== 'user' && (
|
| 78 |
+
<div className="flex-shrink-0 mr-3">
|
| 79 |
+
<div className={`h-8 w-8 rounded-full ${
|
| 80 |
+
isAssistantFallbackMessage ? 'bg-amber-500' : 'bg-primary'
|
| 81 |
+
} flex items-center justify-center text-white`}>
|
| 82 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
| 83 |
+
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
|
| 84 |
+
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
|
| 85 |
+
</svg>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
)}
|
| 89 |
+
|
| 90 |
+
<div
|
| 91 |
+
className={`${
|
| 92 |
+
message.role === 'user'
|
| 93 |
+
? 'bg-primary text-white'
|
| 94 |
+
: isAssistantFallbackMessage
|
| 95 |
+
? 'bg-amber-50 border border-amber-200 text-gray-800'
|
| 96 |
+
: 'bg-white text-gray-800'
|
| 97 |
+
} rounded-lg p-4 shadow-sm max-w-[85%]`}
|
| 98 |
+
>
|
| 99 |
+
<p className="whitespace-pre-wrap">{displayContent}</p>
|
| 100 |
+
|
| 101 |
+
{isAssistantFallbackMessage && isMessageFallback && (
|
| 102 |
+
<p className="mt-2 text-xs text-amber-600 italic">
|
| 103 |
+
(This response was generated using the Qwen fallback model)
|
| 104 |
+
</p>
|
| 105 |
+
)}
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
{message.role === 'user' && (
|
| 109 |
+
<div className="flex-shrink-0 ml-3">
|
| 110 |
+
<div className="h-8 w-8 rounded-full bg-gray-300 flex items-center justify-center text-gray-600">
|
| 111 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
| 112 |
+
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
| 113 |
+
</svg>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
)}
|
| 117 |
+
</div>
|
| 118 |
+
);
|
| 119 |
+
})}
|
| 120 |
+
|
| 121 |
+
<TypingIndicator isVisible={isLoading} />
|
| 122 |
+
</div>
|
| 123 |
+
);
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
export default ChatHistory;
|
OpenAIChatAssistant/client/src/components/ChatInputForm.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { ChatInputFormProps } from '@/lib/types';
|
| 3 |
+
import { Button } from '@/components/ui/button';
|
| 4 |
+
import { Input } from '@/components/ui/input';
|
| 5 |
+
|
| 6 |
+
const ChatInputForm: React.FC<ChatInputFormProps> = ({ onSendMessage, isLoading }) => {
|
| 7 |
+
const [input, setInput] = useState('');
|
| 8 |
+
const inputRef = useRef<HTMLInputElement>(null);
|
| 9 |
+
|
| 10 |
+
// Focus input on component mount
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
if (inputRef.current) {
|
| 13 |
+
inputRef.current.focus();
|
| 14 |
+
}
|
| 15 |
+
}, []);
|
| 16 |
+
|
| 17 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 18 |
+
e.preventDefault();
|
| 19 |
+
|
| 20 |
+
const message = input.trim();
|
| 21 |
+
if (!message || isLoading) return;
|
| 22 |
+
|
| 23 |
+
onSendMessage(message);
|
| 24 |
+
setInput('');
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
const handleClear = () => {
|
| 28 |
+
setInput('');
|
| 29 |
+
if (inputRef.current) {
|
| 30 |
+
inputRef.current.focus();
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
return (
|
| 35 |
+
<form onSubmit={handleSubmit} className="flex items-center space-x-2">
|
| 36 |
+
<div className="relative flex-1">
|
| 37 |
+
<Input
|
| 38 |
+
ref={inputRef}
|
| 39 |
+
type="text"
|
| 40 |
+
value={input}
|
| 41 |
+
onChange={(e) => setInput(e.target.value)}
|
| 42 |
+
placeholder="Type your message here..."
|
| 43 |
+
className="w-full py-3 px-4 bg-gray-100 rounded-full focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white"
|
| 44 |
+
disabled={isLoading}
|
| 45 |
+
/>
|
| 46 |
+
{input && (
|
| 47 |
+
<button
|
| 48 |
+
type="button"
|
| 49 |
+
onClick={handleClear}
|
| 50 |
+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
| 51 |
+
aria-label="Clear input"
|
| 52 |
+
>
|
| 53 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
| 54 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
| 55 |
+
</svg>
|
| 56 |
+
</button>
|
| 57 |
+
)}
|
| 58 |
+
</div>
|
| 59 |
+
<Button
|
| 60 |
+
type="submit"
|
| 61 |
+
disabled={isLoading || !input.trim()}
|
| 62 |
+
className="bg-primary text-white p-3 rounded-full hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 transition-colors duration-200"
|
| 63 |
+
>
|
| 64 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
| 65 |
+
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
|
| 66 |
+
</svg>
|
| 67 |
+
</Button>
|
| 68 |
+
</form>
|
| 69 |
+
);
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
export default ChatInputForm;
|
OpenAIChatAssistant/client/src/components/ConnectionStatus.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { ConnectionStatusProps } from '@/lib/types';
|
| 3 |
+
import { Badge } from '@/components/ui/badge';
|
| 4 |
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
| 5 |
+
|
| 6 |
+
const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ isConnected, currentModel = 'openai' }) => {
|
| 7 |
+
// Get model status details
|
| 8 |
+
const getModelBadge = () => {
|
| 9 |
+
switch (currentModel) {
|
| 10 |
+
case 'openai':
|
| 11 |
+
return (
|
| 12 |
+
<Badge variant="outline" className="ml-2 bg-blue-50 text-blue-800 border-blue-300">
|
| 13 |
+
OpenAI
|
| 14 |
+
</Badge>
|
| 15 |
+
);
|
| 16 |
+
case 'qwen':
|
| 17 |
+
return (
|
| 18 |
+
<Badge variant="outline" className="ml-2 bg-amber-50 text-amber-800 border-amber-300">
|
| 19 |
+
Qwen (Fallback)
|
| 20 |
+
</Badge>
|
| 21 |
+
);
|
| 22 |
+
case 'unavailable':
|
| 23 |
+
return (
|
| 24 |
+
<Badge variant="outline" className="ml-2 bg-red-50 text-red-800 border-red-300">
|
| 25 |
+
No AI Available
|
| 26 |
+
</Badge>
|
| 27 |
+
);
|
| 28 |
+
default:
|
| 29 |
+
return null;
|
| 30 |
+
}
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<div className="flex items-center">
|
| 35 |
+
<span
|
| 36 |
+
className={`inline-block h-2 w-2 rounded-full mr-2 ${
|
| 37 |
+
isConnected ? 'bg-green-500' : 'bg-red-500'
|
| 38 |
+
}`}
|
| 39 |
+
></span>
|
| 40 |
+
<span className="text-sm text-gray-600 mr-2">
|
| 41 |
+
{isConnected ? 'Connected' : 'Disconnected'}
|
| 42 |
+
</span>
|
| 43 |
+
|
| 44 |
+
<TooltipProvider>
|
| 45 |
+
<Tooltip>
|
| 46 |
+
<TooltipTrigger asChild>
|
| 47 |
+
{getModelBadge()}
|
| 48 |
+
</TooltipTrigger>
|
| 49 |
+
<TooltipContent>
|
| 50 |
+
<p>
|
| 51 |
+
{currentModel === 'openai'
|
| 52 |
+
? 'Using OpenAI GPT-4o model'
|
| 53 |
+
: currentModel === 'qwen'
|
| 54 |
+
? 'Using Qwen fallback model due to OpenAI unavailability'
|
| 55 |
+
: 'All AI models are currently unavailable'}
|
| 56 |
+
</p>
|
| 57 |
+
</TooltipContent>
|
| 58 |
+
</Tooltip>
|
| 59 |
+
</TooltipProvider>
|
| 60 |
+
</div>
|
| 61 |
+
);
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
export default ConnectionStatus;
|
OpenAIChatAssistant/client/src/components/ConversationSidebar.tsx
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { PlusCircle, MessageSquare, Trash2, Edit2, Save, X, User, LogOut } from 'lucide-react';
|
| 3 |
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
| 4 |
+
import { Button } from '@/components/ui/button';
|
| 5 |
+
import { Input } from '@/components/ui/input';
|
| 6 |
+
import { apiRequest } from '@/lib/queryClient';
|
| 7 |
+
import { Conversation } from '@/lib/types';
|
| 8 |
+
import { useAuth } from '@/hooks/use-auth';
|
| 9 |
+
import { useLocation } from 'wouter';
|
| 10 |
+
|
| 11 |
+
interface ConversationSidebarProps {
|
| 12 |
+
isOpen: boolean;
|
| 13 |
+
onClose: () => void;
|
| 14 |
+
selectedConversationId: string;
|
| 15 |
+
onSelectConversation: (conversationId: string) => void;
|
| 16 |
+
onNewConversation: () => void;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const ConversationSidebar: React.FC<ConversationSidebarProps> = ({
|
| 20 |
+
isOpen,
|
| 21 |
+
onClose,
|
| 22 |
+
selectedConversationId,
|
| 23 |
+
onSelectConversation,
|
| 24 |
+
onNewConversation
|
| 25 |
+
}) => {
|
| 26 |
+
const [conversations, setConversations] = useState<Conversation[]>([]);
|
| 27 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 28 |
+
const [editingId, setEditingId] = useState<string | null>(null);
|
| 29 |
+
const [editingTitle, setEditingTitle] = useState('');
|
| 30 |
+
const { user, logoutMutation } = useAuth();
|
| 31 |
+
const [, setLocation] = useLocation();
|
| 32 |
+
|
| 33 |
+
const isSignedIn = !!user;
|
| 34 |
+
|
| 35 |
+
// Fetch conversations
|
| 36 |
+
useEffect(() => {
|
| 37 |
+
const fetchConversations = async () => {
|
| 38 |
+
setIsLoading(true);
|
| 39 |
+
try {
|
| 40 |
+
const response = await fetch('/api/conversations');
|
| 41 |
+
if (response.ok) {
|
| 42 |
+
const data = await response.json();
|
| 43 |
+
setConversations(data);
|
| 44 |
+
} else {
|
| 45 |
+
console.error('Failed to fetch conversations');
|
| 46 |
+
}
|
| 47 |
+
} catch (error) {
|
| 48 |
+
console.error('Error fetching conversations:', error);
|
| 49 |
+
} finally {
|
| 50 |
+
setIsLoading(false);
|
| 51 |
+
}
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
fetchConversations();
|
| 55 |
+
|
| 56 |
+
// Set up interval to refresh conversations (every 30 seconds)
|
| 57 |
+
const interval = setInterval(fetchConversations, 30000);
|
| 58 |
+
return () => clearInterval(interval);
|
| 59 |
+
}, [isSignedIn]);
|
| 60 |
+
|
| 61 |
+
// Navigate to auth page
|
| 62 |
+
const handleSignIn = () => {
|
| 63 |
+
setLocation('/auth');
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
// Sign out
|
| 67 |
+
const handleSignOut = () => {
|
| 68 |
+
setLocation('/logout');
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
// Start editing a conversation title
|
| 72 |
+
const handleEditStart = (conversation: Conversation) => {
|
| 73 |
+
setEditingId(conversation.id);
|
| 74 |
+
setEditingTitle(conversation.title);
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
// Cancel editing
|
| 78 |
+
const handleEditCancel = () => {
|
| 79 |
+
setEditingId(null);
|
| 80 |
+
setEditingTitle('');
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
// Save edited title
|
| 84 |
+
const handleEditSave = async (conversationId: string) => {
|
| 85 |
+
try {
|
| 86 |
+
const response = await apiRequest('PATCH', `/api/conversations/${conversationId}/title`, {
|
| 87 |
+
title: editingTitle
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
if (response.ok) {
|
| 91 |
+
const updatedConversation = await response.json();
|
| 92 |
+
setConversations(conversations.map(conv =>
|
| 93 |
+
conv.id === conversationId ? updatedConversation : conv
|
| 94 |
+
));
|
| 95 |
+
setEditingId(null);
|
| 96 |
+
} else {
|
| 97 |
+
console.error('Failed to update conversation title');
|
| 98 |
+
}
|
| 99 |
+
} catch (error) {
|
| 100 |
+
console.error('Error updating conversation title:', error);
|
| 101 |
+
}
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
// Delete a conversation
|
| 105 |
+
const handleDelete = async (conversationId: string) => {
|
| 106 |
+
// Confirm delete
|
| 107 |
+
if (!window.confirm('Are you sure you want to delete this conversation?')) {
|
| 108 |
+
return;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
try {
|
| 112 |
+
const response = await apiRequest('DELETE', `/api/conversations/${conversationId}`);
|
| 113 |
+
|
| 114 |
+
if (response.ok) {
|
| 115 |
+
setConversations(conversations.filter(conv => conv.id !== conversationId));
|
| 116 |
+
|
| 117 |
+
// If we deleted the selected conversation, switch to a new one
|
| 118 |
+
if (conversationId === selectedConversationId) {
|
| 119 |
+
const nextConv = conversations.find(conv => conv.id !== conversationId);
|
| 120 |
+
if (nextConv) {
|
| 121 |
+
onSelectConversation(nextConv.id);
|
| 122 |
+
} else {
|
| 123 |
+
onNewConversation();
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
} else {
|
| 127 |
+
console.error('Failed to delete conversation');
|
| 128 |
+
}
|
| 129 |
+
} catch (error) {
|
| 130 |
+
console.error('Error deleting conversation:', error);
|
| 131 |
+
}
|
| 132 |
+
};
|
| 133 |
+
|
| 134 |
+
return (
|
| 135 |
+
<aside
|
| 136 |
+
className={`fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 shadow-md transform ${
|
| 137 |
+
isOpen ? 'translate-x-0' : '-translate-x-full'
|
| 138 |
+
} transition-transform duration-300 ease-in-out z-10 flex flex-col`}
|
| 139 |
+
>
|
| 140 |
+
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
| 141 |
+
<h2 className="text-lg font-semibold">Conversations</h2>
|
| 142 |
+
<button
|
| 143 |
+
onClick={onClose}
|
| 144 |
+
className="p-1 rounded-full hover:bg-gray-100"
|
| 145 |
+
aria-label="Close sidebar"
|
| 146 |
+
>
|
| 147 |
+
<X className="h-5 w-5 text-gray-500" />
|
| 148 |
+
</button>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<div className="p-4 border-b border-gray-200">
|
| 152 |
+
<Button
|
| 153 |
+
onClick={onNewConversation}
|
| 154 |
+
className="w-full flex items-center justify-center"
|
| 155 |
+
variant="outline"
|
| 156 |
+
>
|
| 157 |
+
<PlusCircle className="mr-2 h-4 w-4" />
|
| 158 |
+
New Chat
|
| 159 |
+
</Button>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
{/* Sign in/out section */}
|
| 163 |
+
<div className="p-4 border-b border-gray-200">
|
| 164 |
+
{isSignedIn ? (
|
| 165 |
+
<Button
|
| 166 |
+
onClick={handleSignOut}
|
| 167 |
+
variant="ghost"
|
| 168 |
+
className="w-full flex items-center justify-center text-red-500 hover:text-red-700 hover:bg-red-50"
|
| 169 |
+
>
|
| 170 |
+
<LogOut className="mr-2 h-4 w-4" />
|
| 171 |
+
Sign Out
|
| 172 |
+
</Button>
|
| 173 |
+
) : (
|
| 174 |
+
<Button
|
| 175 |
+
onClick={handleSignIn}
|
| 176 |
+
variant="default"
|
| 177 |
+
className="w-full flex items-center justify-center"
|
| 178 |
+
>
|
| 179 |
+
<User className="mr-2 h-4 w-4" />
|
| 180 |
+
Sign In to Save Chats
|
| 181 |
+
</Button>
|
| 182 |
+
)}
|
| 183 |
+
{!isSignedIn && (
|
| 184 |
+
<p className="text-xs text-gray-500 mt-2 text-center">
|
| 185 |
+
Create an account to save your conversations
|
| 186 |
+
</p>
|
| 187 |
+
)}
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
{/* Conversations list */}
|
| 191 |
+
<div className="flex-1 overflow-y-auto p-2">
|
| 192 |
+
{isLoading ? (
|
| 193 |
+
<div className="flex items-center justify-center h-20">
|
| 194 |
+
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary"></div>
|
| 195 |
+
</div>
|
| 196 |
+
) : (
|
| 197 |
+
conversations.length === 0 ? (
|
| 198 |
+
isSignedIn ? (
|
| 199 |
+
<div className="text-center text-gray-500 py-8">
|
| 200 |
+
<MessageSquare className="mx-auto h-12 w-12 text-gray-300 mb-2" />
|
| 201 |
+
<p>No conversations yet</p>
|
| 202 |
+
<p className="text-sm">Start a new chat to get started</p>
|
| 203 |
+
</div>
|
| 204 |
+
) : (
|
| 205 |
+
<div className="text-center text-gray-500 py-8">
|
| 206 |
+
<p className="text-sm">Sign in to view your saved conversations</p>
|
| 207 |
+
</div>
|
| 208 |
+
)
|
| 209 |
+
) : (
|
| 210 |
+
<ul className="space-y-1">
|
| 211 |
+
{conversations.map(conversation => (
|
| 212 |
+
<li key={conversation.id} className="relative">
|
| 213 |
+
{editingId === conversation.id ? (
|
| 214 |
+
<div className="flex items-center p-2 rounded-md bg-gray-100">
|
| 215 |
+
<Input
|
| 216 |
+
value={editingTitle}
|
| 217 |
+
onChange={(e) => setEditingTitle(e.target.value)}
|
| 218 |
+
className="flex-1 mr-1"
|
| 219 |
+
autoFocus
|
| 220 |
+
/>
|
| 221 |
+
<div className="flex">
|
| 222 |
+
<Button
|
| 223 |
+
onClick={() => handleEditSave(conversation.id)}
|
| 224 |
+
size="sm"
|
| 225 |
+
variant="ghost"
|
| 226 |
+
className="p-1 h-8 w-8"
|
| 227 |
+
>
|
| 228 |
+
<Save className="h-4 w-4 text-green-500" />
|
| 229 |
+
</Button>
|
| 230 |
+
<Button
|
| 231 |
+
onClick={handleEditCancel}
|
| 232 |
+
size="sm"
|
| 233 |
+
variant="ghost"
|
| 234 |
+
className="p-1 h-8 w-8"
|
| 235 |
+
>
|
| 236 |
+
<X className="h-4 w-4 text-red-500" />
|
| 237 |
+
</Button>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
) : (
|
| 241 |
+
<div
|
| 242 |
+
className={`flex items-center p-2 rounded-md cursor-pointer group ${
|
| 243 |
+
conversation.id === selectedConversationId
|
| 244 |
+
? 'bg-primary text-white'
|
| 245 |
+
: 'hover:bg-gray-100'
|
| 246 |
+
}`}
|
| 247 |
+
onClick={() => onSelectConversation(conversation.id)}
|
| 248 |
+
>
|
| 249 |
+
<MessageSquare className={`h-4 w-4 mr-2 ${
|
| 250 |
+
conversation.id === selectedConversationId ? 'text-white' : 'text-gray-500'
|
| 251 |
+
}`} />
|
| 252 |
+
<span className="flex-1 truncate">{conversation.title}</span>
|
| 253 |
+
|
| 254 |
+
<div className={`flex space-x-1 ${
|
| 255 |
+
conversation.id === selectedConversationId ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
| 256 |
+
} transition-opacity`}>
|
| 257 |
+
<TooltipProvider>
|
| 258 |
+
<Tooltip>
|
| 259 |
+
<TooltipTrigger asChild>
|
| 260 |
+
<Button
|
| 261 |
+
onClick={(e) => {
|
| 262 |
+
e.stopPropagation();
|
| 263 |
+
handleEditStart(conversation);
|
| 264 |
+
}}
|
| 265 |
+
size="sm"
|
| 266 |
+
variant="ghost"
|
| 267 |
+
className={`p-1 h-6 w-6 ${
|
| 268 |
+
conversation.id === selectedConversationId ? 'text-white hover:bg-primary-dark' : ''
|
| 269 |
+
}`}
|
| 270 |
+
>
|
| 271 |
+
<Edit2 className="h-3 w-3" />
|
| 272 |
+
</Button>
|
| 273 |
+
</TooltipTrigger>
|
| 274 |
+
<TooltipContent>
|
| 275 |
+
<p>Edit title</p>
|
| 276 |
+
</TooltipContent>
|
| 277 |
+
</Tooltip>
|
| 278 |
+
</TooltipProvider>
|
| 279 |
+
|
| 280 |
+
<TooltipProvider>
|
| 281 |
+
<Tooltip>
|
| 282 |
+
<TooltipTrigger asChild>
|
| 283 |
+
<Button
|
| 284 |
+
onClick={(e) => {
|
| 285 |
+
e.stopPropagation();
|
| 286 |
+
handleDelete(conversation.id);
|
| 287 |
+
}}
|
| 288 |
+
size="sm"
|
| 289 |
+
variant="ghost"
|
| 290 |
+
className={`p-1 h-6 w-6 ${
|
| 291 |
+
conversation.id === selectedConversationId ? 'text-white hover:bg-primary-dark' : ''
|
| 292 |
+
}`}
|
| 293 |
+
>
|
| 294 |
+
<Trash2 className="h-3 w-3" />
|
| 295 |
+
</Button>
|
| 296 |
+
</TooltipTrigger>
|
| 297 |
+
<TooltipContent>
|
| 298 |
+
<p>Delete conversation</p>
|
| 299 |
+
</TooltipContent>
|
| 300 |
+
</Tooltip>
|
| 301 |
+
</TooltipProvider>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
)}
|
| 305 |
+
</li>
|
| 306 |
+
))}
|
| 307 |
+
</ul>
|
| 308 |
+
)
|
| 309 |
+
)}
|
| 310 |
+
</div>
|
| 311 |
+
</aside>
|
| 312 |
+
);
|
| 313 |
+
};
|
| 314 |
+
|
| 315 |
+
export default ConversationSidebar;
|
OpenAIChatAssistant/client/src/components/ImageGenerator.tsx
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { useForm } from 'react-hook-form';
|
| 3 |
+
import { zodResolver } from '@hookform/resolvers/zod';
|
| 4 |
+
import { z } from 'zod';
|
| 5 |
+
import { Button } from './ui/button';
|
| 6 |
+
import {
|
| 7 |
+
Form,
|
| 8 |
+
FormControl,
|
| 9 |
+
FormDescription,
|
| 10 |
+
FormField,
|
| 11 |
+
FormItem,
|
| 12 |
+
FormLabel,
|
| 13 |
+
FormMessage,
|
| 14 |
+
} from './ui/form';
|
| 15 |
+
import { Input } from './ui/input';
|
| 16 |
+
import { Slider } from './ui/slider';
|
| 17 |
+
import { Switch } from './ui/switch';
|
| 18 |
+
import { cn } from '@/lib/utils';
|
| 19 |
+
import { Card } from './ui/card';
|
| 20 |
+
import { Loader2 } from 'lucide-react';
|
| 21 |
+
|
| 22 |
+
// Define form schema
|
| 23 |
+
const formSchema = z.object({
|
| 24 |
+
prompt: z.string().min(1, {
|
| 25 |
+
message: 'Prompt is required',
|
| 26 |
+
}).max(1000, {
|
| 27 |
+
message: 'Prompt must be less than 1000 characters',
|
| 28 |
+
}),
|
| 29 |
+
width: z.number().min(256).max(1024).default(512),
|
| 30 |
+
height: z.number().min(256).max(1024).default(512),
|
| 31 |
+
seed: z.number().default(0),
|
| 32 |
+
randomize_seed: z.boolean().default(true),
|
| 33 |
+
guidance_scale: z.number().min(0).max(20).default(7.5),
|
| 34 |
+
num_inference_steps: z.number().min(1).max(50).default(20),
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
type FormValues = z.infer<typeof formSchema>;
|
| 38 |
+
|
| 39 |
+
export default function ImageGenerator() {
|
| 40 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 41 |
+
const [error, setError] = useState<string | null>(null);
|
| 42 |
+
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
| 43 |
+
|
| 44 |
+
// Default form values
|
| 45 |
+
const defaultValues: FormValues = {
|
| 46 |
+
prompt: '',
|
| 47 |
+
width: 512,
|
| 48 |
+
height: 512,
|
| 49 |
+
seed: 0,
|
| 50 |
+
randomize_seed: true,
|
| 51 |
+
guidance_scale: 7.5,
|
| 52 |
+
num_inference_steps: 20,
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
// Initialize form
|
| 56 |
+
const form = useForm<FormValues>({
|
| 57 |
+
resolver: zodResolver(formSchema),
|
| 58 |
+
defaultValues,
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// Handle form submission
|
| 62 |
+
const onSubmit = async (data: FormValues) => {
|
| 63 |
+
setIsLoading(true);
|
| 64 |
+
setError(null);
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
const response = await fetch('/api/generate-image', {
|
| 68 |
+
method: 'POST',
|
| 69 |
+
headers: {
|
| 70 |
+
'Content-Type': 'application/json',
|
| 71 |
+
},
|
| 72 |
+
body: JSON.stringify(data),
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
if (!response.ok) {
|
| 76 |
+
const errorData = await response.json();
|
| 77 |
+
throw new Error(errorData.message || 'Failed to generate image');
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
const result = await response.json();
|
| 81 |
+
setImageUrl(result.imageUrl);
|
| 82 |
+
} catch (err: any) {
|
| 83 |
+
setError(err.message || 'An error occurred while generating the image');
|
| 84 |
+
console.error('Error generating image:', err);
|
| 85 |
+
} finally {
|
| 86 |
+
setIsLoading(false);
|
| 87 |
+
}
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
return (
|
| 91 |
+
<div className="space-y-6">
|
| 92 |
+
<div className="flex justify-between items-center">
|
| 93 |
+
<h2 className="text-3xl font-bold">Image Generator</h2>
|
| 94 |
+
{isLoading && <div className="flex items-center"><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Generating...</div>}
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 98 |
+
<div>
|
| 99 |
+
<Form {...form}>
|
| 100 |
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
| 101 |
+
<FormField
|
| 102 |
+
control={form.control}
|
| 103 |
+
name="prompt"
|
| 104 |
+
render={({ field }) => (
|
| 105 |
+
<FormItem>
|
| 106 |
+
<FormLabel>Prompt</FormLabel>
|
| 107 |
+
<FormControl>
|
| 108 |
+
<Input placeholder="Enter your prompt..." {...field} />
|
| 109 |
+
</FormControl>
|
| 110 |
+
<FormDescription>
|
| 111 |
+
Describe the image you want to generate
|
| 112 |
+
</FormDescription>
|
| 113 |
+
<FormMessage />
|
| 114 |
+
</FormItem>
|
| 115 |
+
)}
|
| 116 |
+
/>
|
| 117 |
+
|
| 118 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 119 |
+
<FormField
|
| 120 |
+
control={form.control}
|
| 121 |
+
name="width"
|
| 122 |
+
render={({ field }) => (
|
| 123 |
+
<FormItem>
|
| 124 |
+
<FormLabel>Width: {field.value}px</FormLabel>
|
| 125 |
+
<FormControl>
|
| 126 |
+
<Slider
|
| 127 |
+
min={256}
|
| 128 |
+
max={1024}
|
| 129 |
+
step={64}
|
| 130 |
+
defaultValue={[field.value]}
|
| 131 |
+
onValueChange={(value) => field.onChange(value[0])}
|
| 132 |
+
/>
|
| 133 |
+
</FormControl>
|
| 134 |
+
<FormMessage />
|
| 135 |
+
</FormItem>
|
| 136 |
+
)}
|
| 137 |
+
/>
|
| 138 |
+
|
| 139 |
+
<FormField
|
| 140 |
+
control={form.control}
|
| 141 |
+
name="height"
|
| 142 |
+
render={({ field }) => (
|
| 143 |
+
<FormItem>
|
| 144 |
+
<FormLabel>Height: {field.value}px</FormLabel>
|
| 145 |
+
<FormControl>
|
| 146 |
+
<Slider
|
| 147 |
+
min={256}
|
| 148 |
+
max={1024}
|
| 149 |
+
step={64}
|
| 150 |
+
defaultValue={[field.value]}
|
| 151 |
+
onValueChange={(value) => field.onChange(value[0])}
|
| 152 |
+
/>
|
| 153 |
+
</FormControl>
|
| 154 |
+
<FormMessage />
|
| 155 |
+
</FormItem>
|
| 156 |
+
)}
|
| 157 |
+
/>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<FormField
|
| 161 |
+
control={form.control}
|
| 162 |
+
name="randomize_seed"
|
| 163 |
+
render={({ field }) => (
|
| 164 |
+
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
| 165 |
+
<div className="space-y-0.5">
|
| 166 |
+
<FormLabel className="text-base">Randomize Seed</FormLabel>
|
| 167 |
+
<FormDescription>
|
| 168 |
+
Use a random seed for each generation
|
| 169 |
+
</FormDescription>
|
| 170 |
+
</div>
|
| 171 |
+
<FormControl>
|
| 172 |
+
<Switch
|
| 173 |
+
checked={field.value}
|
| 174 |
+
onCheckedChange={field.onChange}
|
| 175 |
+
/>
|
| 176 |
+
</FormControl>
|
| 177 |
+
</FormItem>
|
| 178 |
+
)}
|
| 179 |
+
/>
|
| 180 |
+
|
| 181 |
+
{!form.watch("randomize_seed") && (
|
| 182 |
+
<FormField
|
| 183 |
+
control={form.control}
|
| 184 |
+
name="seed"
|
| 185 |
+
render={({ field }) => (
|
| 186 |
+
<FormItem>
|
| 187 |
+
<FormLabel>Seed: {field.value}</FormLabel>
|
| 188 |
+
<FormControl>
|
| 189 |
+
<Input
|
| 190 |
+
type="number"
|
| 191 |
+
{...field}
|
| 192 |
+
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
| 193 |
+
/>
|
| 194 |
+
</FormControl>
|
| 195 |
+
<FormDescription>
|
| 196 |
+
Specific seed for reproducible results
|
| 197 |
+
</FormDescription>
|
| 198 |
+
<FormMessage />
|
| 199 |
+
</FormItem>
|
| 200 |
+
)}
|
| 201 |
+
/>
|
| 202 |
+
)}
|
| 203 |
+
|
| 204 |
+
<FormField
|
| 205 |
+
control={form.control}
|
| 206 |
+
name="guidance_scale"
|
| 207 |
+
render={({ field }) => (
|
| 208 |
+
<FormItem>
|
| 209 |
+
<FormLabel>Guidance Scale: {field.value}</FormLabel>
|
| 210 |
+
<FormControl>
|
| 211 |
+
<Slider
|
| 212 |
+
min={0}
|
| 213 |
+
max={20}
|
| 214 |
+
step={0.1}
|
| 215 |
+
defaultValue={[field.value]}
|
| 216 |
+
onValueChange={(value) => field.onChange(value[0])}
|
| 217 |
+
/>
|
| 218 |
+
</FormControl>
|
| 219 |
+
<FormDescription>
|
| 220 |
+
How closely to follow the prompt (higher = more faithful)
|
| 221 |
+
</FormDescription>
|
| 222 |
+
<FormMessage />
|
| 223 |
+
</FormItem>
|
| 224 |
+
)}
|
| 225 |
+
/>
|
| 226 |
+
|
| 227 |
+
<FormField
|
| 228 |
+
control={form.control}
|
| 229 |
+
name="num_inference_steps"
|
| 230 |
+
render={({ field }) => (
|
| 231 |
+
<FormItem>
|
| 232 |
+
<FormLabel>Inference Steps: {field.value}</FormLabel>
|
| 233 |
+
<FormControl>
|
| 234 |
+
<Slider
|
| 235 |
+
min={1}
|
| 236 |
+
max={50}
|
| 237 |
+
step={1}
|
| 238 |
+
defaultValue={[field.value]}
|
| 239 |
+
onValueChange={(value) => field.onChange(value[0])}
|
| 240 |
+
/>
|
| 241 |
+
</FormControl>
|
| 242 |
+
<FormDescription>
|
| 243 |
+
Number of denoising steps (higher = better quality but slower)
|
| 244 |
+
</FormDescription>
|
| 245 |
+
<FormMessage />
|
| 246 |
+
</FormItem>
|
| 247 |
+
)}
|
| 248 |
+
/>
|
| 249 |
+
|
| 250 |
+
{error && (
|
| 251 |
+
<div className="bg-red-50 text-red-500 p-3 rounded-md text-sm">
|
| 252 |
+
{error}
|
| 253 |
+
</div>
|
| 254 |
+
)}
|
| 255 |
+
|
| 256 |
+
<Button
|
| 257 |
+
type="submit"
|
| 258 |
+
className="w-full"
|
| 259 |
+
disabled={isLoading}
|
| 260 |
+
>
|
| 261 |
+
{isLoading ? (
|
| 262 |
+
<>
|
| 263 |
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
| 264 |
+
Generating...
|
| 265 |
+
</>
|
| 266 |
+
) : (
|
| 267 |
+
'Generate Image'
|
| 268 |
+
)}
|
| 269 |
+
</Button>
|
| 270 |
+
</form>
|
| 271 |
+
</Form>
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
<div className={cn("flex flex-col items-center justify-center",
|
| 275 |
+
imageUrl ? "bg-gray-50 dark:bg-gray-900" : "bg-gray-100 dark:bg-gray-800")}>
|
| 276 |
+
{imageUrl ? (
|
| 277 |
+
<div className="relative w-full">
|
| 278 |
+
<img
|
| 279 |
+
src={imageUrl}
|
| 280 |
+
alt="Generated"
|
| 281 |
+
className="rounded-md object-contain max-h-[600px] mx-auto"
|
| 282 |
+
/>
|
| 283 |
+
<div className="mt-4 flex justify-center">
|
| 284 |
+
<Button
|
| 285 |
+
variant="outline"
|
| 286 |
+
onClick={() => window.open(imageUrl, '_blank')}
|
| 287 |
+
className="mr-2"
|
| 288 |
+
>
|
| 289 |
+
Open in New Tab
|
| 290 |
+
</Button>
|
| 291 |
+
<Button
|
| 292 |
+
variant="outline"
|
| 293 |
+
onClick={() => {
|
| 294 |
+
const a = document.createElement('a');
|
| 295 |
+
a.href = imageUrl;
|
| 296 |
+
a.download = 'generated-image.png';
|
| 297 |
+
document.body.appendChild(a);
|
| 298 |
+
a.click();
|
| 299 |
+
document.body.removeChild(a);
|
| 300 |
+
}}
|
| 301 |
+
>
|
| 302 |
+
Download
|
| 303 |
+
</Button>
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
) : (
|
| 307 |
+
<Card className="w-full h-full flex items-center justify-center p-8 border-dashed">
|
| 308 |
+
<div className="text-center">
|
| 309 |
+
<p className="text-muted-foreground">
|
| 310 |
+
Your generated image will appear here
|
| 311 |
+
</p>
|
| 312 |
+
</div>
|
| 313 |
+
</Card>
|
| 314 |
+
)}
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
);
|
| 319 |
+
}
|
OpenAIChatAssistant/client/src/components/TypingIndicator.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { TypingIndicatorProps } from '@/lib/types';
|
| 3 |
+
|
| 4 |
+
const TypingIndicator: React.FC<TypingIndicatorProps> = ({ isVisible }) => {
|
| 5 |
+
if (!isVisible) return null;
|
| 6 |
+
|
| 7 |
+
return (
|
| 8 |
+
<div className="flex items-start mb-4">
|
| 9 |
+
<div className="flex-shrink-0 mr-3">
|
| 10 |
+
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-white">
|
| 11 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
| 12 |
+
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
|
| 13 |
+
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
|
| 14 |
+
</svg>
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
<div className="bg-white rounded-lg shadow-sm">
|
| 18 |
+
<div className="flex items-center p-2">
|
| 19 |
+
<div className="flex space-x-1">
|
| 20 |
+
<span className="h-2 w-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0s' }}></span>
|
| 21 |
+
<span className="h-2 w-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></span>
|
| 22 |
+
<span className="h-2 w-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></span>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
);
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export default TypingIndicator;
|
OpenAIChatAssistant/client/src/components/UserSettingsModal.tsx
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect } from "react";
|
| 2 |
+
import { useForm } from "react-hook-form";
|
| 3 |
+
import { zodResolver } from "@hookform/resolvers/zod";
|
| 4 |
+
import { z } from "zod";
|
| 5 |
+
import { useMutation } from "@tanstack/react-query";
|
| 6 |
+
import { apiRequest, queryClient } from "@/lib/queryClient";
|
| 7 |
+
import { useAuth } from "@/hooks/use-auth";
|
| 8 |
+
import { useToast } from "@/hooks/use-toast";
|
| 9 |
+
import { updateUserProfileSchema } from "@shared/schema";
|
| 10 |
+
|
| 11 |
+
import {
|
| 12 |
+
Dialog,
|
| 13 |
+
DialogContent,
|
| 14 |
+
DialogDescription,
|
| 15 |
+
DialogHeader,
|
| 16 |
+
DialogTitle,
|
| 17 |
+
DialogFooter,
|
| 18 |
+
} from "@/components/ui/dialog";
|
| 19 |
+
import {
|
| 20 |
+
Form,
|
| 21 |
+
FormControl,
|
| 22 |
+
FormDescription,
|
| 23 |
+
FormField,
|
| 24 |
+
FormItem,
|
| 25 |
+
FormLabel,
|
| 26 |
+
FormMessage,
|
| 27 |
+
} from "@/components/ui/form";
|
| 28 |
+
import { Input } from "@/components/ui/input";
|
| 29 |
+
import { Textarea } from "@/components/ui/textarea";
|
| 30 |
+
import { Button } from "@/components/ui/button";
|
| 31 |
+
import { Loader2 } from "lucide-react";
|
| 32 |
+
|
| 33 |
+
// Create a form schema
|
| 34 |
+
const profileFormSchema = z.object({
|
| 35 |
+
fullName: z.string().optional(),
|
| 36 |
+
location: z.string().optional(),
|
| 37 |
+
interests: z.array(z.string()).optional(),
|
| 38 |
+
interestsInput: z.string().optional(), // For input field value only, not submitted
|
| 39 |
+
profession: z.string().optional(),
|
| 40 |
+
pets: z.string().optional(),
|
| 41 |
+
systemContext: z.string().optional(),
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
type ProfileFormValues = z.infer<typeof profileFormSchema>;
|
| 45 |
+
|
| 46 |
+
interface UserSettingsModalProps {
|
| 47 |
+
isOpen: boolean;
|
| 48 |
+
onClose: () => void;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export default function UserSettingsModal({
|
| 52 |
+
isOpen,
|
| 53 |
+
onClose,
|
| 54 |
+
}: UserSettingsModalProps) {
|
| 55 |
+
const { user } = useAuth();
|
| 56 |
+
const { toast } = useToast();
|
| 57 |
+
|
| 58 |
+
// Create form with default values
|
| 59 |
+
const form = useForm<ProfileFormValues>({
|
| 60 |
+
resolver: zodResolver(profileFormSchema),
|
| 61 |
+
defaultValues: {
|
| 62 |
+
fullName: "",
|
| 63 |
+
location: "",
|
| 64 |
+
interests: [],
|
| 65 |
+
interestsInput: "",
|
| 66 |
+
profession: "",
|
| 67 |
+
pets: "",
|
| 68 |
+
systemContext: "",
|
| 69 |
+
},
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
// Update form when user data changes
|
| 73 |
+
useEffect(() => {
|
| 74 |
+
if (user) {
|
| 75 |
+
// Convert interests array to comma-separated string for display
|
| 76 |
+
const interestsString = user.interests?.join(", ") || "";
|
| 77 |
+
|
| 78 |
+
form.reset({
|
| 79 |
+
fullName: user.fullName || "",
|
| 80 |
+
location: user.location || "",
|
| 81 |
+
interests: user.interests || [],
|
| 82 |
+
interestsInput: interestsString,
|
| 83 |
+
profession: user.profession || "",
|
| 84 |
+
pets: user.pets || "",
|
| 85 |
+
systemContext: user.systemContext || "",
|
| 86 |
+
});
|
| 87 |
+
}
|
| 88 |
+
}, [user, form]);
|
| 89 |
+
|
| 90 |
+
const updateProfileMutation = useMutation({
|
| 91 |
+
mutationFn: async (data: ProfileFormValues) => {
|
| 92 |
+
const res = await apiRequest("PATCH", "/api/user/profile", data);
|
| 93 |
+
if (!res.ok) {
|
| 94 |
+
const errorData = await res.json();
|
| 95 |
+
throw new Error(errorData.message || "Failed to update profile");
|
| 96 |
+
}
|
| 97 |
+
return await res.json();
|
| 98 |
+
},
|
| 99 |
+
onSuccess: (updatedUser) => {
|
| 100 |
+
queryClient.setQueryData(["/api/user"], updatedUser);
|
| 101 |
+
toast({
|
| 102 |
+
title: "Profile updated",
|
| 103 |
+
description: "Your profile has been updated successfully.",
|
| 104 |
+
});
|
| 105 |
+
onClose();
|
| 106 |
+
},
|
| 107 |
+
onError: (error: Error) => {
|
| 108 |
+
toast({
|
| 109 |
+
title: "Update failed",
|
| 110 |
+
description: error.message,
|
| 111 |
+
variant: "destructive",
|
| 112 |
+
});
|
| 113 |
+
},
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
const onSubmit = async (data: ProfileFormValues) => {
|
| 117 |
+
// Create a copy of the data object without interestsInput
|
| 118 |
+
const { interestsInput, ...submitData } = data;
|
| 119 |
+
|
| 120 |
+
// Submit data without the temporary interestsInput field
|
| 121 |
+
await updateProfileMutation.mutateAsync(submitData);
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
// Convert string to array for interests field if needed
|
| 125 |
+
const handleInterestsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 126 |
+
const value = e.target.value;
|
| 127 |
+
// Just store the input value as is, don't process it yet
|
| 128 |
+
form.setValue("interestsInput", value);
|
| 129 |
+
|
| 130 |
+
// Process for the actual interests field that gets submitted
|
| 131 |
+
const interestsArray = value
|
| 132 |
+
.split(",")
|
| 133 |
+
.map((item) => item.trim())
|
| 134 |
+
.filter((item) => item !== "");
|
| 135 |
+
form.setValue("interests", interestsArray);
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
return (
|
| 139 |
+
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
| 140 |
+
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
| 141 |
+
<DialogHeader>
|
| 142 |
+
<DialogTitle>User Settings</DialogTitle>
|
| 143 |
+
<DialogDescription>
|
| 144 |
+
Update your profile information and AI assistant preferences
|
| 145 |
+
</DialogDescription>
|
| 146 |
+
</DialogHeader>
|
| 147 |
+
|
| 148 |
+
<Form {...form}>
|
| 149 |
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
| 150 |
+
<div className="space-y-4">
|
| 151 |
+
{/* Profile Information Section */}
|
| 152 |
+
<div className="border-b pb-2">
|
| 153 |
+
<h3 className="text-lg font-medium">Profile Information</h3>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<FormField
|
| 157 |
+
control={form.control}
|
| 158 |
+
name="fullName"
|
| 159 |
+
render={({ field }) => (
|
| 160 |
+
<FormItem>
|
| 161 |
+
<FormLabel>Full Name</FormLabel>
|
| 162 |
+
<FormControl>
|
| 163 |
+
<Input placeholder="Your full name" {...field} value={field.value || ""} />
|
| 164 |
+
</FormControl>
|
| 165 |
+
<FormMessage />
|
| 166 |
+
</FormItem>
|
| 167 |
+
)}
|
| 168 |
+
/>
|
| 169 |
+
|
| 170 |
+
<FormField
|
| 171 |
+
control={form.control}
|
| 172 |
+
name="location"
|
| 173 |
+
render={({ field }) => (
|
| 174 |
+
<FormItem>
|
| 175 |
+
<FormLabel>Location</FormLabel>
|
| 176 |
+
<FormControl>
|
| 177 |
+
<Input placeholder="Your location" {...field} value={field.value || ""} />
|
| 178 |
+
</FormControl>
|
| 179 |
+
<FormMessage />
|
| 180 |
+
</FormItem>
|
| 181 |
+
)}
|
| 182 |
+
/>
|
| 183 |
+
|
| 184 |
+
<FormField
|
| 185 |
+
control={form.control}
|
| 186 |
+
name="profession"
|
| 187 |
+
render={({ field }) => (
|
| 188 |
+
<FormItem>
|
| 189 |
+
<FormLabel>Profession</FormLabel>
|
| 190 |
+
<FormControl>
|
| 191 |
+
<Input placeholder="Your profession" {...field} value={field.value || ""} />
|
| 192 |
+
</FormControl>
|
| 193 |
+
<FormMessage />
|
| 194 |
+
</FormItem>
|
| 195 |
+
)}
|
| 196 |
+
/>
|
| 197 |
+
|
| 198 |
+
<FormField
|
| 199 |
+
control={form.control}
|
| 200 |
+
name="interestsInput"
|
| 201 |
+
render={({ field }) => (
|
| 202 |
+
<FormItem>
|
| 203 |
+
<FormLabel>Interests</FormLabel>
|
| 204 |
+
<FormControl>
|
| 205 |
+
<Input
|
| 206 |
+
placeholder="Interests (comma-separated)"
|
| 207 |
+
{...field}
|
| 208 |
+
onChange={handleInterestsChange}
|
| 209 |
+
/>
|
| 210 |
+
</FormControl>
|
| 211 |
+
<FormDescription>
|
| 212 |
+
Enter your interests separated by commas
|
| 213 |
+
</FormDescription>
|
| 214 |
+
<FormMessage />
|
| 215 |
+
</FormItem>
|
| 216 |
+
)}
|
| 217 |
+
/>
|
| 218 |
+
|
| 219 |
+
<FormField
|
| 220 |
+
control={form.control}
|
| 221 |
+
name="pets"
|
| 222 |
+
render={({ field }) => (
|
| 223 |
+
<FormItem>
|
| 224 |
+
<FormLabel>Pets</FormLabel>
|
| 225 |
+
<FormControl>
|
| 226 |
+
<Input placeholder="Your pets" {...field} value={field.value || ""} />
|
| 227 |
+
</FormControl>
|
| 228 |
+
<FormMessage />
|
| 229 |
+
</FormItem>
|
| 230 |
+
)}
|
| 231 |
+
/>
|
| 232 |
+
|
| 233 |
+
{/* AI Assistant Preferences Section */}
|
| 234 |
+
<div className="border-b pb-2 pt-4">
|
| 235 |
+
<h3 className="text-lg font-medium">AI Assistant Preferences</h3>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<FormField
|
| 239 |
+
control={form.control}
|
| 240 |
+
name="additionalInfo"
|
| 241 |
+
render={({ field }) => (
|
| 242 |
+
<FormItem>
|
| 243 |
+
<FormLabel>Additional Information</FormLabel>
|
| 244 |
+
<FormControl>
|
| 245 |
+
<Textarea
|
| 246 |
+
placeholder="Add any additional information about yourself that you'd like the AI to know"
|
| 247 |
+
className="min-h-[100px]"
|
| 248 |
+
{...field}
|
| 249 |
+
value={field.value || ""}
|
| 250 |
+
/>
|
| 251 |
+
</FormControl>
|
| 252 |
+
<FormDescription>
|
| 253 |
+
This information will be included in your AI context
|
| 254 |
+
</FormDescription>
|
| 255 |
+
<FormMessage />
|
| 256 |
+
</FormItem>
|
| 257 |
+
)}
|
| 258 |
+
/>
|
| 259 |
+
|
| 260 |
+
<div className="space-y-4">
|
| 261 |
+
<div className="flex justify-between items-center">
|
| 262 |
+
<FormLabel className="text-base">System Context</FormLabel>
|
| 263 |
+
<Button
|
| 264 |
+
type="button"
|
| 265 |
+
variant="outline"
|
| 266 |
+
size="sm"
|
| 267 |
+
onClick={() => {
|
| 268 |
+
// Generate structured context from profile fields
|
| 269 |
+
const fullName = form.getValues("fullName");
|
| 270 |
+
const location = form.getValues("location");
|
| 271 |
+
const interests = form.getValues("interests");
|
| 272 |
+
const profession = form.getValues("profession");
|
| 273 |
+
const pets = form.getValues("pets");
|
| 274 |
+
const additionalInfo = form.getValues("additionalInfo");
|
| 275 |
+
|
| 276 |
+
// Format profile information in a structured way
|
| 277 |
+
let profileInfo = "";
|
| 278 |
+
if (fullName) profileInfo += `name: ${fullName}\n`;
|
| 279 |
+
if (location) profileInfo += `location: ${location}\n`;
|
| 280 |
+
if (interests && interests.length > 0) profileInfo += `interests: ${interests.join(", ")}\n`;
|
| 281 |
+
if (profession) profileInfo += `profession: ${profession}\n`;
|
| 282 |
+
if (pets) profileInfo += `pets: ${pets}\n`;
|
| 283 |
+
if (additionalInfo) profileInfo += `additional_info: ${additionalInfo}\n`;
|
| 284 |
+
|
| 285 |
+
// Get existing context
|
| 286 |
+
const currentContext = form.getValues("systemContext") || "";
|
| 287 |
+
|
| 288 |
+
// Set the new structured context
|
| 289 |
+
form.setValue("systemContext", profileInfo + "\n" + currentContext);
|
| 290 |
+
}}
|
| 291 |
+
>
|
| 292 |
+
Include Profile Info
|
| 293 |
+
</Button>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
<FormField
|
| 297 |
+
control={form.control}
|
| 298 |
+
name="systemContext"
|
| 299 |
+
render={({ field }) => (
|
| 300 |
+
<FormItem>
|
| 301 |
+
<FormControl>
|
| 302 |
+
<Textarea
|
| 303 |
+
placeholder="Add custom context for the AI assistant to understand your requirements better"
|
| 304 |
+
className="min-h-[150px]"
|
| 305 |
+
{...field}
|
| 306 |
+
value={field.value || ""}
|
| 307 |
+
/>
|
| 308 |
+
</FormControl>
|
| 309 |
+
<FormDescription>
|
| 310 |
+
This context will be provided to the AI assistant for all your conversations.
|
| 311 |
+
Use key-value pairs like "name: Your Name" for best results.
|
| 312 |
+
</FormDescription>
|
| 313 |
+
<FormMessage />
|
| 314 |
+
</FormItem>
|
| 315 |
+
)}
|
| 316 |
+
/>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
|
| 320 |
+
<DialogFooter>
|
| 321 |
+
<Button
|
| 322 |
+
type="button"
|
| 323 |
+
variant="outline"
|
| 324 |
+
onClick={onClose}
|
| 325 |
+
disabled={updateProfileMutation.isPending}
|
| 326 |
+
>
|
| 327 |
+
Cancel
|
| 328 |
+
</Button>
|
| 329 |
+
<Button
|
| 330 |
+
type="submit"
|
| 331 |
+
disabled={updateProfileMutation.isPending}
|
| 332 |
+
>
|
| 333 |
+
{updateProfileMutation.isPending ? (
|
| 334 |
+
<>
|
| 335 |
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
| 336 |
+
Saving...
|
| 337 |
+
</>
|
| 338 |
+
) : (
|
| 339 |
+
"Save Changes"
|
| 340 |
+
)}
|
| 341 |
+
</Button>
|
| 342 |
+
</DialogFooter>
|
| 343 |
+
</form>
|
| 344 |
+
</Form>
|
| 345 |
+
</DialogContent>
|
| 346 |
+
</Dialog>
|
| 347 |
+
);
|
| 348 |
+
}
|
OpenAIChatAssistant/client/src/components/VideoGenerator.tsx
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { useForm } from 'react-hook-form';
|
| 3 |
+
import { zodResolver } from '@hookform/resolvers/zod';
|
| 4 |
+
import { z } from 'zod';
|
| 5 |
+
import { Button } from './ui/button';
|
| 6 |
+
import {
|
| 7 |
+
Form,
|
| 8 |
+
FormControl,
|
| 9 |
+
FormDescription,
|
| 10 |
+
FormField,
|
| 11 |
+
FormItem,
|
| 12 |
+
FormLabel,
|
| 13 |
+
FormMessage,
|
| 14 |
+
} from './ui/form';
|
| 15 |
+
import { Input } from './ui/input';
|
| 16 |
+
import { cn } from '@/lib/utils';
|
| 17 |
+
import { Card } from './ui/card';
|
| 18 |
+
import { Loader2 } from 'lucide-react';
|
| 19 |
+
|
| 20 |
+
// Define form schema
|
| 21 |
+
const formSchema = z.object({
|
| 22 |
+
prompt: z.string().min(1, {
|
| 23 |
+
message: 'Prompt is required',
|
| 24 |
+
}).max(1000, {
|
| 25 |
+
message: 'Prompt must be less than 1000 characters',
|
| 26 |
+
}),
|
| 27 |
+
model: z.enum(["Wan-AI/Wan2.1-T2V-14B"]).default("Wan-AI/Wan2.1-T2V-14B"),
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
type FormValues = z.infer<typeof formSchema>;
|
| 31 |
+
|
| 32 |
+
export default function VideoGenerator() {
|
| 33 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 34 |
+
const [error, setError] = useState<string | null>(null);
|
| 35 |
+
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
| 36 |
+
|
| 37 |
+
// Default form values
|
| 38 |
+
const defaultValues: FormValues = {
|
| 39 |
+
prompt: '',
|
| 40 |
+
model: "Wan-AI/Wan2.1-T2V-14B",
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
// Initialize form
|
| 44 |
+
const form = useForm<FormValues>({
|
| 45 |
+
resolver: zodResolver(formSchema),
|
| 46 |
+
defaultValues,
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
// Handle form submission
|
| 50 |
+
const onSubmit = async (data: FormValues) => {
|
| 51 |
+
setIsLoading(true);
|
| 52 |
+
setError(null);
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
const response = await fetch('/api/generate-video', {
|
| 56 |
+
method: 'POST',
|
| 57 |
+
headers: {
|
| 58 |
+
'Content-Type': 'application/json',
|
| 59 |
+
},
|
| 60 |
+
body: JSON.stringify(data),
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
if (!response.ok) {
|
| 64 |
+
const errorData = await response.json();
|
| 65 |
+
throw new Error(errorData.message || 'Failed to generate video');
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
const result = await response.json();
|
| 69 |
+
setVideoUrl(result.videoUrl);
|
| 70 |
+
} catch (err: any) {
|
| 71 |
+
setError(err.message || 'An error occurred while generating the video');
|
| 72 |
+
console.error('Error generating video:', err);
|
| 73 |
+
} finally {
|
| 74 |
+
setIsLoading(false);
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<div className="w-full space-y-8">
|
| 80 |
+
<Card className="p-6">
|
| 81 |
+
<Form {...form}>
|
| 82 |
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
| 83 |
+
<FormField
|
| 84 |
+
control={form.control}
|
| 85 |
+
name="prompt"
|
| 86 |
+
render={({ field }) => (
|
| 87 |
+
<FormItem>
|
| 88 |
+
<FormLabel>Prompt</FormLabel>
|
| 89 |
+
<FormControl>
|
| 90 |
+
<Input
|
| 91 |
+
placeholder="A young man walking on the street"
|
| 92 |
+
{...field}
|
| 93 |
+
/>
|
| 94 |
+
</FormControl>
|
| 95 |
+
<FormDescription>
|
| 96 |
+
Describe the video you want to generate.
|
| 97 |
+
</FormDescription>
|
| 98 |
+
<FormMessage />
|
| 99 |
+
</FormItem>
|
| 100 |
+
)}
|
| 101 |
+
/>
|
| 102 |
+
|
| 103 |
+
<Button
|
| 104 |
+
type="submit"
|
| 105 |
+
className="w-full"
|
| 106 |
+
disabled={isLoading}
|
| 107 |
+
>
|
| 108 |
+
{isLoading ? (
|
| 109 |
+
<>
|
| 110 |
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
| 111 |
+
Generating video...
|
| 112 |
+
</>
|
| 113 |
+
) : 'Generate Video'}
|
| 114 |
+
</Button>
|
| 115 |
+
</form>
|
| 116 |
+
</Form>
|
| 117 |
+
</Card>
|
| 118 |
+
|
| 119 |
+
{error && (
|
| 120 |
+
<div className="p-4 text-sm border border-red-200 bg-red-50 text-red-800 rounded-md">
|
| 121 |
+
{error}
|
| 122 |
+
</div>
|
| 123 |
+
)}
|
| 124 |
+
|
| 125 |
+
<div className={cn("flex flex-col items-center justify-center",
|
| 126 |
+
videoUrl ? "bg-gray-50 dark:bg-gray-900" : "bg-gray-100 dark:bg-gray-800")}>
|
| 127 |
+
{videoUrl ? (
|
| 128 |
+
<div className="relative w-full">
|
| 129 |
+
<video
|
| 130 |
+
src={videoUrl}
|
| 131 |
+
controls
|
| 132 |
+
className="rounded-md object-contain max-h-[600px] mx-auto"
|
| 133 |
+
/>
|
| 134 |
+
<div className="mt-4 flex justify-center">
|
| 135 |
+
<Button
|
| 136 |
+
variant="outline"
|
| 137 |
+
onClick={() => window.open(videoUrl, '_blank')}
|
| 138 |
+
className="mr-2"
|
| 139 |
+
>
|
| 140 |
+
Open in New Tab
|
| 141 |
+
</Button>
|
| 142 |
+
<Button
|
| 143 |
+
variant="outline"
|
| 144 |
+
onClick={() => {
|
| 145 |
+
const a = document.createElement('a');
|
| 146 |
+
a.href = videoUrl;
|
| 147 |
+
a.download = 'generated-video.mp4';
|
| 148 |
+
document.body.appendChild(a);
|
| 149 |
+
a.click();
|
| 150 |
+
document.body.removeChild(a);
|
| 151 |
+
}}
|
| 152 |
+
>
|
| 153 |
+
Download
|
| 154 |
+
</Button>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
) : (
|
| 158 |
+
<Card className="w-full h-full flex items-center justify-center p-8 border-dashed">
|
| 159 |
+
<div className="text-center">
|
| 160 |
+
<p className="text-muted-foreground">
|
| 161 |
+
Your generated video will appear here
|
| 162 |
+
</p>
|
| 163 |
+
</div>
|
| 164 |
+
</Card>
|
| 165 |
+
)}
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
);
|
| 169 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/accordion.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
| 3 |
+
import { ChevronDown } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Accordion = AccordionPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const AccordionItem = React.forwardRef<
|
| 10 |
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
| 11 |
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
| 12 |
+
>(({ className, ...props }, ref) => (
|
| 13 |
+
<AccordionPrimitive.Item
|
| 14 |
+
ref={ref}
|
| 15 |
+
className={cn("border-b", className)}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
))
|
| 19 |
+
AccordionItem.displayName = "AccordionItem"
|
| 20 |
+
|
| 21 |
+
const AccordionTrigger = React.forwardRef<
|
| 22 |
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
| 23 |
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
| 24 |
+
>(({ className, children, ...props }, ref) => (
|
| 25 |
+
<AccordionPrimitive.Header className="flex">
|
| 26 |
+
<AccordionPrimitive.Trigger
|
| 27 |
+
ref={ref}
|
| 28 |
+
className={cn(
|
| 29 |
+
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
| 30 |
+
className
|
| 31 |
+
)}
|
| 32 |
+
{...props}
|
| 33 |
+
>
|
| 34 |
+
{children}
|
| 35 |
+
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
| 36 |
+
</AccordionPrimitive.Trigger>
|
| 37 |
+
</AccordionPrimitive.Header>
|
| 38 |
+
))
|
| 39 |
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
| 40 |
+
|
| 41 |
+
const AccordionContent = React.forwardRef<
|
| 42 |
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
| 43 |
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
| 44 |
+
>(({ className, children, ...props }, ref) => (
|
| 45 |
+
<AccordionPrimitive.Content
|
| 46 |
+
ref={ref}
|
| 47 |
+
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
| 48 |
+
{...props}
|
| 49 |
+
>
|
| 50 |
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
| 51 |
+
</AccordionPrimitive.Content>
|
| 52 |
+
))
|
| 53 |
+
|
| 54 |
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
| 55 |
+
|
| 56 |
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
OpenAIChatAssistant/client/src/components/ui/alert-dialog.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
import { buttonVariants } from "@/components/ui/button"
|
| 6 |
+
|
| 7 |
+
const AlertDialog = AlertDialogPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
| 12 |
+
|
| 13 |
+
const AlertDialogOverlay = React.forwardRef<
|
| 14 |
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
| 15 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
| 16 |
+
>(({ className, ...props }, ref) => (
|
| 17 |
+
<AlertDialogPrimitive.Overlay
|
| 18 |
+
className={cn(
|
| 19 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
| 20 |
+
className
|
| 21 |
+
)}
|
| 22 |
+
{...props}
|
| 23 |
+
ref={ref}
|
| 24 |
+
/>
|
| 25 |
+
))
|
| 26 |
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
| 27 |
+
|
| 28 |
+
const AlertDialogContent = React.forwardRef<
|
| 29 |
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
| 30 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
| 31 |
+
>(({ className, ...props }, ref) => (
|
| 32 |
+
<AlertDialogPortal>
|
| 33 |
+
<AlertDialogOverlay />
|
| 34 |
+
<AlertDialogPrimitive.Content
|
| 35 |
+
ref={ref}
|
| 36 |
+
className={cn(
|
| 37 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
| 38 |
+
className
|
| 39 |
+
)}
|
| 40 |
+
{...props}
|
| 41 |
+
/>
|
| 42 |
+
</AlertDialogPortal>
|
| 43 |
+
))
|
| 44 |
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
| 45 |
+
|
| 46 |
+
const AlertDialogHeader = ({
|
| 47 |
+
className,
|
| 48 |
+
...props
|
| 49 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 50 |
+
<div
|
| 51 |
+
className={cn(
|
| 52 |
+
"flex flex-col space-y-2 text-center sm:text-left",
|
| 53 |
+
className
|
| 54 |
+
)}
|
| 55 |
+
{...props}
|
| 56 |
+
/>
|
| 57 |
+
)
|
| 58 |
+
AlertDialogHeader.displayName = "AlertDialogHeader"
|
| 59 |
+
|
| 60 |
+
const AlertDialogFooter = ({
|
| 61 |
+
className,
|
| 62 |
+
...props
|
| 63 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 64 |
+
<div
|
| 65 |
+
className={cn(
|
| 66 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
| 67 |
+
className
|
| 68 |
+
)}
|
| 69 |
+
{...props}
|
| 70 |
+
/>
|
| 71 |
+
)
|
| 72 |
+
AlertDialogFooter.displayName = "AlertDialogFooter"
|
| 73 |
+
|
| 74 |
+
const AlertDialogTitle = React.forwardRef<
|
| 75 |
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
| 76 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
| 77 |
+
>(({ className, ...props }, ref) => (
|
| 78 |
+
<AlertDialogPrimitive.Title
|
| 79 |
+
ref={ref}
|
| 80 |
+
className={cn("text-lg font-semibold", className)}
|
| 81 |
+
{...props}
|
| 82 |
+
/>
|
| 83 |
+
))
|
| 84 |
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
| 85 |
+
|
| 86 |
+
const AlertDialogDescription = React.forwardRef<
|
| 87 |
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
| 88 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
| 89 |
+
>(({ className, ...props }, ref) => (
|
| 90 |
+
<AlertDialogPrimitive.Description
|
| 91 |
+
ref={ref}
|
| 92 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 93 |
+
{...props}
|
| 94 |
+
/>
|
| 95 |
+
))
|
| 96 |
+
AlertDialogDescription.displayName =
|
| 97 |
+
AlertDialogPrimitive.Description.displayName
|
| 98 |
+
|
| 99 |
+
const AlertDialogAction = React.forwardRef<
|
| 100 |
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
| 101 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
| 102 |
+
>(({ className, ...props }, ref) => (
|
| 103 |
+
<AlertDialogPrimitive.Action
|
| 104 |
+
ref={ref}
|
| 105 |
+
className={cn(buttonVariants(), className)}
|
| 106 |
+
{...props}
|
| 107 |
+
/>
|
| 108 |
+
))
|
| 109 |
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
| 110 |
+
|
| 111 |
+
const AlertDialogCancel = React.forwardRef<
|
| 112 |
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
| 113 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
| 114 |
+
>(({ className, ...props }, ref) => (
|
| 115 |
+
<AlertDialogPrimitive.Cancel
|
| 116 |
+
ref={ref}
|
| 117 |
+
className={cn(
|
| 118 |
+
buttonVariants({ variant: "outline" }),
|
| 119 |
+
"mt-2 sm:mt-0",
|
| 120 |
+
className
|
| 121 |
+
)}
|
| 122 |
+
{...props}
|
| 123 |
+
/>
|
| 124 |
+
))
|
| 125 |
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
| 126 |
+
|
| 127 |
+
export {
|
| 128 |
+
AlertDialog,
|
| 129 |
+
AlertDialogPortal,
|
| 130 |
+
AlertDialogOverlay,
|
| 131 |
+
AlertDialogTrigger,
|
| 132 |
+
AlertDialogContent,
|
| 133 |
+
AlertDialogHeader,
|
| 134 |
+
AlertDialogFooter,
|
| 135 |
+
AlertDialogTitle,
|
| 136 |
+
AlertDialogDescription,
|
| 137 |
+
AlertDialogAction,
|
| 138 |
+
AlertDialogCancel,
|
| 139 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/alert.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const alertVariants = cva(
|
| 7 |
+
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
| 8 |
+
{
|
| 9 |
+
variants: {
|
| 10 |
+
variant: {
|
| 11 |
+
default: "bg-background text-foreground",
|
| 12 |
+
destructive:
|
| 13 |
+
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
defaultVariants: {
|
| 17 |
+
variant: "default",
|
| 18 |
+
},
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
const Alert = React.forwardRef<
|
| 23 |
+
HTMLDivElement,
|
| 24 |
+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
| 25 |
+
>(({ className, variant, ...props }, ref) => (
|
| 26 |
+
<div
|
| 27 |
+
ref={ref}
|
| 28 |
+
role="alert"
|
| 29 |
+
className={cn(alertVariants({ variant }), className)}
|
| 30 |
+
{...props}
|
| 31 |
+
/>
|
| 32 |
+
))
|
| 33 |
+
Alert.displayName = "Alert"
|
| 34 |
+
|
| 35 |
+
const AlertTitle = React.forwardRef<
|
| 36 |
+
HTMLParagraphElement,
|
| 37 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
| 38 |
+
>(({ className, ...props }, ref) => (
|
| 39 |
+
<h5
|
| 40 |
+
ref={ref}
|
| 41 |
+
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
| 42 |
+
{...props}
|
| 43 |
+
/>
|
| 44 |
+
))
|
| 45 |
+
AlertTitle.displayName = "AlertTitle"
|
| 46 |
+
|
| 47 |
+
const AlertDescription = React.forwardRef<
|
| 48 |
+
HTMLParagraphElement,
|
| 49 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
| 50 |
+
>(({ className, ...props }, ref) => (
|
| 51 |
+
<div
|
| 52 |
+
ref={ref}
|
| 53 |
+
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
))
|
| 57 |
+
AlertDescription.displayName = "AlertDescription"
|
| 58 |
+
|
| 59 |
+
export { Alert, AlertTitle, AlertDescription }
|
OpenAIChatAssistant/client/src/components/ui/aspect-ratio.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
| 2 |
+
|
| 3 |
+
const AspectRatio = AspectRatioPrimitive.Root
|
| 4 |
+
|
| 5 |
+
export { AspectRatio }
|
OpenAIChatAssistant/client/src/components/ui/avatar.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
const Avatar = React.forwardRef<
|
| 9 |
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
| 10 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
| 11 |
+
>(({ className, ...props }, ref) => (
|
| 12 |
+
<AvatarPrimitive.Root
|
| 13 |
+
ref={ref}
|
| 14 |
+
className={cn(
|
| 15 |
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
| 16 |
+
className
|
| 17 |
+
)}
|
| 18 |
+
{...props}
|
| 19 |
+
/>
|
| 20 |
+
))
|
| 21 |
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
| 22 |
+
|
| 23 |
+
const AvatarImage = React.forwardRef<
|
| 24 |
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
| 25 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
| 26 |
+
>(({ className, ...props }, ref) => (
|
| 27 |
+
<AvatarPrimitive.Image
|
| 28 |
+
ref={ref}
|
| 29 |
+
className={cn("aspect-square h-full w-full", className)}
|
| 30 |
+
{...props}
|
| 31 |
+
/>
|
| 32 |
+
))
|
| 33 |
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
| 34 |
+
|
| 35 |
+
const AvatarFallback = React.forwardRef<
|
| 36 |
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
| 37 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
| 38 |
+
>(({ className, ...props }, ref) => (
|
| 39 |
+
<AvatarPrimitive.Fallback
|
| 40 |
+
ref={ref}
|
| 41 |
+
className={cn(
|
| 42 |
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
| 43 |
+
className
|
| 44 |
+
)}
|
| 45 |
+
{...props}
|
| 46 |
+
/>
|
| 47 |
+
))
|
| 48 |
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
| 49 |
+
|
| 50 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
OpenAIChatAssistant/client/src/components/ui/badge.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const badgeVariants = cva(
|
| 7 |
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
| 8 |
+
{
|
| 9 |
+
variants: {
|
| 10 |
+
variant: {
|
| 11 |
+
default:
|
| 12 |
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
| 13 |
+
secondary:
|
| 14 |
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 15 |
+
destructive:
|
| 16 |
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
| 17 |
+
outline: "text-foreground",
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
defaultVariants: {
|
| 21 |
+
variant: "default",
|
| 22 |
+
},
|
| 23 |
+
}
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
export interface BadgeProps
|
| 27 |
+
extends React.HTMLAttributes<HTMLDivElement>,
|
| 28 |
+
VariantProps<typeof badgeVariants> {}
|
| 29 |
+
|
| 30 |
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
| 31 |
+
return (
|
| 32 |
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
| 33 |
+
)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export { Badge, badgeVariants }
|
OpenAIChatAssistant/client/src/components/ui/breadcrumb.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
+
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Breadcrumb = React.forwardRef<
|
| 8 |
+
HTMLElement,
|
| 9 |
+
React.ComponentPropsWithoutRef<"nav"> & {
|
| 10 |
+
separator?: React.ReactNode
|
| 11 |
+
}
|
| 12 |
+
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
| 13 |
+
Breadcrumb.displayName = "Breadcrumb"
|
| 14 |
+
|
| 15 |
+
const BreadcrumbList = React.forwardRef<
|
| 16 |
+
HTMLOListElement,
|
| 17 |
+
React.ComponentPropsWithoutRef<"ol">
|
| 18 |
+
>(({ className, ...props }, ref) => (
|
| 19 |
+
<ol
|
| 20 |
+
ref={ref}
|
| 21 |
+
className={cn(
|
| 22 |
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
| 23 |
+
className
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
/>
|
| 27 |
+
))
|
| 28 |
+
BreadcrumbList.displayName = "BreadcrumbList"
|
| 29 |
+
|
| 30 |
+
const BreadcrumbItem = React.forwardRef<
|
| 31 |
+
HTMLLIElement,
|
| 32 |
+
React.ComponentPropsWithoutRef<"li">
|
| 33 |
+
>(({ className, ...props }, ref) => (
|
| 34 |
+
<li
|
| 35 |
+
ref={ref}
|
| 36 |
+
className={cn("inline-flex items-center gap-1.5", className)}
|
| 37 |
+
{...props}
|
| 38 |
+
/>
|
| 39 |
+
))
|
| 40 |
+
BreadcrumbItem.displayName = "BreadcrumbItem"
|
| 41 |
+
|
| 42 |
+
const BreadcrumbLink = React.forwardRef<
|
| 43 |
+
HTMLAnchorElement,
|
| 44 |
+
React.ComponentPropsWithoutRef<"a"> & {
|
| 45 |
+
asChild?: boolean
|
| 46 |
+
}
|
| 47 |
+
>(({ asChild, className, ...props }, ref) => {
|
| 48 |
+
const Comp = asChild ? Slot : "a"
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<Comp
|
| 52 |
+
ref={ref}
|
| 53 |
+
className={cn("transition-colors hover:text-foreground", className)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
)
|
| 57 |
+
})
|
| 58 |
+
BreadcrumbLink.displayName = "BreadcrumbLink"
|
| 59 |
+
|
| 60 |
+
const BreadcrumbPage = React.forwardRef<
|
| 61 |
+
HTMLSpanElement,
|
| 62 |
+
React.ComponentPropsWithoutRef<"span">
|
| 63 |
+
>(({ className, ...props }, ref) => (
|
| 64 |
+
<span
|
| 65 |
+
ref={ref}
|
| 66 |
+
role="link"
|
| 67 |
+
aria-disabled="true"
|
| 68 |
+
aria-current="page"
|
| 69 |
+
className={cn("font-normal text-foreground", className)}
|
| 70 |
+
{...props}
|
| 71 |
+
/>
|
| 72 |
+
))
|
| 73 |
+
BreadcrumbPage.displayName = "BreadcrumbPage"
|
| 74 |
+
|
| 75 |
+
const BreadcrumbSeparator = ({
|
| 76 |
+
children,
|
| 77 |
+
className,
|
| 78 |
+
...props
|
| 79 |
+
}: React.ComponentProps<"li">) => (
|
| 80 |
+
<li
|
| 81 |
+
role="presentation"
|
| 82 |
+
aria-hidden="true"
|
| 83 |
+
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
| 84 |
+
{...props}
|
| 85 |
+
>
|
| 86 |
+
{children ?? <ChevronRight />}
|
| 87 |
+
</li>
|
| 88 |
+
)
|
| 89 |
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
| 90 |
+
|
| 91 |
+
const BreadcrumbEllipsis = ({
|
| 92 |
+
className,
|
| 93 |
+
...props
|
| 94 |
+
}: React.ComponentProps<"span">) => (
|
| 95 |
+
<span
|
| 96 |
+
role="presentation"
|
| 97 |
+
aria-hidden="true"
|
| 98 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
| 99 |
+
{...props}
|
| 100 |
+
>
|
| 101 |
+
<MoreHorizontal className="h-4 w-4" />
|
| 102 |
+
<span className="sr-only">More</span>
|
| 103 |
+
</span>
|
| 104 |
+
)
|
| 105 |
+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
| 106 |
+
|
| 107 |
+
export {
|
| 108 |
+
Breadcrumb,
|
| 109 |
+
BreadcrumbList,
|
| 110 |
+
BreadcrumbItem,
|
| 111 |
+
BreadcrumbLink,
|
| 112 |
+
BreadcrumbPage,
|
| 113 |
+
BreadcrumbSeparator,
|
| 114 |
+
BreadcrumbEllipsis,
|
| 115 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/button.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const buttonVariants = cva(
|
| 8 |
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 9 |
+
{
|
| 10 |
+
variants: {
|
| 11 |
+
variant: {
|
| 12 |
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
| 13 |
+
destructive:
|
| 14 |
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
| 15 |
+
outline:
|
| 16 |
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
| 17 |
+
secondary:
|
| 18 |
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 19 |
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 20 |
+
link: "text-primary underline-offset-4 hover:underline",
|
| 21 |
+
},
|
| 22 |
+
size: {
|
| 23 |
+
default: "h-10 px-4 py-2",
|
| 24 |
+
sm: "h-9 rounded-md px-3",
|
| 25 |
+
lg: "h-11 rounded-md px-8",
|
| 26 |
+
icon: "h-10 w-10",
|
| 27 |
+
},
|
| 28 |
+
},
|
| 29 |
+
defaultVariants: {
|
| 30 |
+
variant: "default",
|
| 31 |
+
size: "default",
|
| 32 |
+
},
|
| 33 |
+
}
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
export interface ButtonProps
|
| 37 |
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
| 38 |
+
VariantProps<typeof buttonVariants> {
|
| 39 |
+
asChild?: boolean
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
| 43 |
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
| 44 |
+
const Comp = asChild ? Slot : "button"
|
| 45 |
+
return (
|
| 46 |
+
<Comp
|
| 47 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
| 48 |
+
ref={ref}
|
| 49 |
+
{...props}
|
| 50 |
+
/>
|
| 51 |
+
)
|
| 52 |
+
}
|
| 53 |
+
)
|
| 54 |
+
Button.displayName = "Button"
|
| 55 |
+
|
| 56 |
+
export { Button, buttonVariants }
|
OpenAIChatAssistant/client/src/components/ui/calendar.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
| 3 |
+
import { DayPicker } from "react-day-picker"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
import { buttonVariants } from "@/components/ui/button"
|
| 7 |
+
|
| 8 |
+
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
| 9 |
+
|
| 10 |
+
function Calendar({
|
| 11 |
+
className,
|
| 12 |
+
classNames,
|
| 13 |
+
showOutsideDays = true,
|
| 14 |
+
...props
|
| 15 |
+
}: CalendarProps) {
|
| 16 |
+
return (
|
| 17 |
+
<DayPicker
|
| 18 |
+
showOutsideDays={showOutsideDays}
|
| 19 |
+
className={cn("p-3", className)}
|
| 20 |
+
classNames={{
|
| 21 |
+
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
| 22 |
+
month: "space-y-4",
|
| 23 |
+
caption: "flex justify-center pt-1 relative items-center",
|
| 24 |
+
caption_label: "text-sm font-medium",
|
| 25 |
+
nav: "space-x-1 flex items-center",
|
| 26 |
+
nav_button: cn(
|
| 27 |
+
buttonVariants({ variant: "outline" }),
|
| 28 |
+
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
| 29 |
+
),
|
| 30 |
+
nav_button_previous: "absolute left-1",
|
| 31 |
+
nav_button_next: "absolute right-1",
|
| 32 |
+
table: "w-full border-collapse space-y-1",
|
| 33 |
+
head_row: "flex",
|
| 34 |
+
head_cell:
|
| 35 |
+
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
| 36 |
+
row: "flex w-full mt-2",
|
| 37 |
+
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
| 38 |
+
day: cn(
|
| 39 |
+
buttonVariants({ variant: "ghost" }),
|
| 40 |
+
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
| 41 |
+
),
|
| 42 |
+
day_range_end: "day-range-end",
|
| 43 |
+
day_selected:
|
| 44 |
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
| 45 |
+
day_today: "bg-accent text-accent-foreground",
|
| 46 |
+
day_outside:
|
| 47 |
+
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
| 48 |
+
day_disabled: "text-muted-foreground opacity-50",
|
| 49 |
+
day_range_middle:
|
| 50 |
+
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
| 51 |
+
day_hidden: "invisible",
|
| 52 |
+
...classNames,
|
| 53 |
+
}}
|
| 54 |
+
components={{
|
| 55 |
+
IconLeft: ({ className, ...props }) => (
|
| 56 |
+
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
| 57 |
+
),
|
| 58 |
+
IconRight: ({ className, ...props }) => (
|
| 59 |
+
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
| 60 |
+
),
|
| 61 |
+
}}
|
| 62 |
+
{...props}
|
| 63 |
+
/>
|
| 64 |
+
)
|
| 65 |
+
}
|
| 66 |
+
Calendar.displayName = "Calendar"
|
| 67 |
+
|
| 68 |
+
export { Calendar }
|
OpenAIChatAssistant/client/src/components/ui/card.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
const Card = React.forwardRef<
|
| 6 |
+
HTMLDivElement,
|
| 7 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 8 |
+
>(({ className, ...props }, ref) => (
|
| 9 |
+
<div
|
| 10 |
+
ref={ref}
|
| 11 |
+
className={cn(
|
| 12 |
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
{...props}
|
| 16 |
+
/>
|
| 17 |
+
))
|
| 18 |
+
Card.displayName = "Card"
|
| 19 |
+
|
| 20 |
+
const CardHeader = React.forwardRef<
|
| 21 |
+
HTMLDivElement,
|
| 22 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 23 |
+
>(({ className, ...props }, ref) => (
|
| 24 |
+
<div
|
| 25 |
+
ref={ref}
|
| 26 |
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
| 27 |
+
{...props}
|
| 28 |
+
/>
|
| 29 |
+
))
|
| 30 |
+
CardHeader.displayName = "CardHeader"
|
| 31 |
+
|
| 32 |
+
const CardTitle = React.forwardRef<
|
| 33 |
+
HTMLParagraphElement,
|
| 34 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
| 35 |
+
>(({ className, ...props }, ref) => (
|
| 36 |
+
<h3
|
| 37 |
+
ref={ref}
|
| 38 |
+
className={cn(
|
| 39 |
+
"text-2xl font-semibold leading-none tracking-tight",
|
| 40 |
+
className
|
| 41 |
+
)}
|
| 42 |
+
{...props}
|
| 43 |
+
/>
|
| 44 |
+
))
|
| 45 |
+
CardTitle.displayName = "CardTitle"
|
| 46 |
+
|
| 47 |
+
const CardDescription = React.forwardRef<
|
| 48 |
+
HTMLParagraphElement,
|
| 49 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
| 50 |
+
>(({ className, ...props }, ref) => (
|
| 51 |
+
<p
|
| 52 |
+
ref={ref}
|
| 53 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
))
|
| 57 |
+
CardDescription.displayName = "CardDescription"
|
| 58 |
+
|
| 59 |
+
const CardContent = React.forwardRef<
|
| 60 |
+
HTMLDivElement,
|
| 61 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 62 |
+
>(({ className, ...props }, ref) => (
|
| 63 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
| 64 |
+
))
|
| 65 |
+
CardContent.displayName = "CardContent"
|
| 66 |
+
|
| 67 |
+
const CardFooter = React.forwardRef<
|
| 68 |
+
HTMLDivElement,
|
| 69 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 70 |
+
>(({ className, ...props }, ref) => (
|
| 71 |
+
<div
|
| 72 |
+
ref={ref}
|
| 73 |
+
className={cn("flex items-center p-6 pt-0", className)}
|
| 74 |
+
{...props}
|
| 75 |
+
/>
|
| 76 |
+
))
|
| 77 |
+
CardFooter.displayName = "CardFooter"
|
| 78 |
+
|
| 79 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
OpenAIChatAssistant/client/src/components/ui/carousel.tsx
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import useEmblaCarousel, {
|
| 3 |
+
type UseEmblaCarouselType,
|
| 4 |
+
} from "embla-carousel-react"
|
| 5 |
+
import { ArrowLeft, ArrowRight } from "lucide-react"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
import { Button } from "@/components/ui/button"
|
| 9 |
+
|
| 10 |
+
type CarouselApi = UseEmblaCarouselType[1]
|
| 11 |
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
| 12 |
+
type CarouselOptions = UseCarouselParameters[0]
|
| 13 |
+
type CarouselPlugin = UseCarouselParameters[1]
|
| 14 |
+
|
| 15 |
+
type CarouselProps = {
|
| 16 |
+
opts?: CarouselOptions
|
| 17 |
+
plugins?: CarouselPlugin
|
| 18 |
+
orientation?: "horizontal" | "vertical"
|
| 19 |
+
setApi?: (api: CarouselApi) => void
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
type CarouselContextProps = {
|
| 23 |
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
| 24 |
+
api: ReturnType<typeof useEmblaCarousel>[1]
|
| 25 |
+
scrollPrev: () => void
|
| 26 |
+
scrollNext: () => void
|
| 27 |
+
canScrollPrev: boolean
|
| 28 |
+
canScrollNext: boolean
|
| 29 |
+
} & CarouselProps
|
| 30 |
+
|
| 31 |
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
| 32 |
+
|
| 33 |
+
function useCarousel() {
|
| 34 |
+
const context = React.useContext(CarouselContext)
|
| 35 |
+
|
| 36 |
+
if (!context) {
|
| 37 |
+
throw new Error("useCarousel must be used within a <Carousel />")
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return context
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const Carousel = React.forwardRef<
|
| 44 |
+
HTMLDivElement,
|
| 45 |
+
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
| 46 |
+
>(
|
| 47 |
+
(
|
| 48 |
+
{
|
| 49 |
+
orientation = "horizontal",
|
| 50 |
+
opts,
|
| 51 |
+
setApi,
|
| 52 |
+
plugins,
|
| 53 |
+
className,
|
| 54 |
+
children,
|
| 55 |
+
...props
|
| 56 |
+
},
|
| 57 |
+
ref
|
| 58 |
+
) => {
|
| 59 |
+
const [carouselRef, api] = useEmblaCarousel(
|
| 60 |
+
{
|
| 61 |
+
...opts,
|
| 62 |
+
axis: orientation === "horizontal" ? "x" : "y",
|
| 63 |
+
},
|
| 64 |
+
plugins
|
| 65 |
+
)
|
| 66 |
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
| 67 |
+
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
| 68 |
+
|
| 69 |
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
| 70 |
+
if (!api) {
|
| 71 |
+
return
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
setCanScrollPrev(api.canScrollPrev())
|
| 75 |
+
setCanScrollNext(api.canScrollNext())
|
| 76 |
+
}, [])
|
| 77 |
+
|
| 78 |
+
const scrollPrev = React.useCallback(() => {
|
| 79 |
+
api?.scrollPrev()
|
| 80 |
+
}, [api])
|
| 81 |
+
|
| 82 |
+
const scrollNext = React.useCallback(() => {
|
| 83 |
+
api?.scrollNext()
|
| 84 |
+
}, [api])
|
| 85 |
+
|
| 86 |
+
const handleKeyDown = React.useCallback(
|
| 87 |
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
| 88 |
+
if (event.key === "ArrowLeft") {
|
| 89 |
+
event.preventDefault()
|
| 90 |
+
scrollPrev()
|
| 91 |
+
} else if (event.key === "ArrowRight") {
|
| 92 |
+
event.preventDefault()
|
| 93 |
+
scrollNext()
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
[scrollPrev, scrollNext]
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
React.useEffect(() => {
|
| 100 |
+
if (!api || !setApi) {
|
| 101 |
+
return
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
setApi(api)
|
| 105 |
+
}, [api, setApi])
|
| 106 |
+
|
| 107 |
+
React.useEffect(() => {
|
| 108 |
+
if (!api) {
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
onSelect(api)
|
| 113 |
+
api.on("reInit", onSelect)
|
| 114 |
+
api.on("select", onSelect)
|
| 115 |
+
|
| 116 |
+
return () => {
|
| 117 |
+
api?.off("select", onSelect)
|
| 118 |
+
}
|
| 119 |
+
}, [api, onSelect])
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<CarouselContext.Provider
|
| 123 |
+
value={{
|
| 124 |
+
carouselRef,
|
| 125 |
+
api: api,
|
| 126 |
+
opts,
|
| 127 |
+
orientation:
|
| 128 |
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
| 129 |
+
scrollPrev,
|
| 130 |
+
scrollNext,
|
| 131 |
+
canScrollPrev,
|
| 132 |
+
canScrollNext,
|
| 133 |
+
}}
|
| 134 |
+
>
|
| 135 |
+
<div
|
| 136 |
+
ref={ref}
|
| 137 |
+
onKeyDownCapture={handleKeyDown}
|
| 138 |
+
className={cn("relative", className)}
|
| 139 |
+
role="region"
|
| 140 |
+
aria-roledescription="carousel"
|
| 141 |
+
{...props}
|
| 142 |
+
>
|
| 143 |
+
{children}
|
| 144 |
+
</div>
|
| 145 |
+
</CarouselContext.Provider>
|
| 146 |
+
)
|
| 147 |
+
}
|
| 148 |
+
)
|
| 149 |
+
Carousel.displayName = "Carousel"
|
| 150 |
+
|
| 151 |
+
const CarouselContent = React.forwardRef<
|
| 152 |
+
HTMLDivElement,
|
| 153 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 154 |
+
>(({ className, ...props }, ref) => {
|
| 155 |
+
const { carouselRef, orientation } = useCarousel()
|
| 156 |
+
|
| 157 |
+
return (
|
| 158 |
+
<div ref={carouselRef} className="overflow-hidden">
|
| 159 |
+
<div
|
| 160 |
+
ref={ref}
|
| 161 |
+
className={cn(
|
| 162 |
+
"flex",
|
| 163 |
+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
| 164 |
+
className
|
| 165 |
+
)}
|
| 166 |
+
{...props}
|
| 167 |
+
/>
|
| 168 |
+
</div>
|
| 169 |
+
)
|
| 170 |
+
})
|
| 171 |
+
CarouselContent.displayName = "CarouselContent"
|
| 172 |
+
|
| 173 |
+
const CarouselItem = React.forwardRef<
|
| 174 |
+
HTMLDivElement,
|
| 175 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 176 |
+
>(({ className, ...props }, ref) => {
|
| 177 |
+
const { orientation } = useCarousel()
|
| 178 |
+
|
| 179 |
+
return (
|
| 180 |
+
<div
|
| 181 |
+
ref={ref}
|
| 182 |
+
role="group"
|
| 183 |
+
aria-roledescription="slide"
|
| 184 |
+
className={cn(
|
| 185 |
+
"min-w-0 shrink-0 grow-0 basis-full",
|
| 186 |
+
orientation === "horizontal" ? "pl-4" : "pt-4",
|
| 187 |
+
className
|
| 188 |
+
)}
|
| 189 |
+
{...props}
|
| 190 |
+
/>
|
| 191 |
+
)
|
| 192 |
+
})
|
| 193 |
+
CarouselItem.displayName = "CarouselItem"
|
| 194 |
+
|
| 195 |
+
const CarouselPrevious = React.forwardRef<
|
| 196 |
+
HTMLButtonElement,
|
| 197 |
+
React.ComponentProps<typeof Button>
|
| 198 |
+
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
| 199 |
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
| 200 |
+
|
| 201 |
+
return (
|
| 202 |
+
<Button
|
| 203 |
+
ref={ref}
|
| 204 |
+
variant={variant}
|
| 205 |
+
size={size}
|
| 206 |
+
className={cn(
|
| 207 |
+
"absolute h-8 w-8 rounded-full",
|
| 208 |
+
orientation === "horizontal"
|
| 209 |
+
? "-left-12 top-1/2 -translate-y-1/2"
|
| 210 |
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
| 211 |
+
className
|
| 212 |
+
)}
|
| 213 |
+
disabled={!canScrollPrev}
|
| 214 |
+
onClick={scrollPrev}
|
| 215 |
+
{...props}
|
| 216 |
+
>
|
| 217 |
+
<ArrowLeft className="h-4 w-4" />
|
| 218 |
+
<span className="sr-only">Previous slide</span>
|
| 219 |
+
</Button>
|
| 220 |
+
)
|
| 221 |
+
})
|
| 222 |
+
CarouselPrevious.displayName = "CarouselPrevious"
|
| 223 |
+
|
| 224 |
+
const CarouselNext = React.forwardRef<
|
| 225 |
+
HTMLButtonElement,
|
| 226 |
+
React.ComponentProps<typeof Button>
|
| 227 |
+
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
| 228 |
+
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
| 229 |
+
|
| 230 |
+
return (
|
| 231 |
+
<Button
|
| 232 |
+
ref={ref}
|
| 233 |
+
variant={variant}
|
| 234 |
+
size={size}
|
| 235 |
+
className={cn(
|
| 236 |
+
"absolute h-8 w-8 rounded-full",
|
| 237 |
+
orientation === "horizontal"
|
| 238 |
+
? "-right-12 top-1/2 -translate-y-1/2"
|
| 239 |
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
| 240 |
+
className
|
| 241 |
+
)}
|
| 242 |
+
disabled={!canScrollNext}
|
| 243 |
+
onClick={scrollNext}
|
| 244 |
+
{...props}
|
| 245 |
+
>
|
| 246 |
+
<ArrowRight className="h-4 w-4" />
|
| 247 |
+
<span className="sr-only">Next slide</span>
|
| 248 |
+
</Button>
|
| 249 |
+
)
|
| 250 |
+
})
|
| 251 |
+
CarouselNext.displayName = "CarouselNext"
|
| 252 |
+
|
| 253 |
+
export {
|
| 254 |
+
type CarouselApi,
|
| 255 |
+
Carousel,
|
| 256 |
+
CarouselContent,
|
| 257 |
+
CarouselItem,
|
| 258 |
+
CarouselPrevious,
|
| 259 |
+
CarouselNext,
|
| 260 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/chart.tsx
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as RechartsPrimitive from "recharts"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
// Format: { THEME_NAME: CSS_SELECTOR }
|
| 9 |
+
const THEMES = { light: "", dark: ".dark" } as const
|
| 10 |
+
|
| 11 |
+
export type ChartConfig = {
|
| 12 |
+
[k in string]: {
|
| 13 |
+
label?: React.ReactNode
|
| 14 |
+
icon?: React.ComponentType
|
| 15 |
+
} & (
|
| 16 |
+
| { color?: string; theme?: never }
|
| 17 |
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
type ChartContextProps = {
|
| 22 |
+
config: ChartConfig
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
| 26 |
+
|
| 27 |
+
function useChart() {
|
| 28 |
+
const context = React.useContext(ChartContext)
|
| 29 |
+
|
| 30 |
+
if (!context) {
|
| 31 |
+
throw new Error("useChart must be used within a <ChartContainer />")
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return context
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const ChartContainer = React.forwardRef<
|
| 38 |
+
HTMLDivElement,
|
| 39 |
+
React.ComponentProps<"div"> & {
|
| 40 |
+
config: ChartConfig
|
| 41 |
+
children: React.ComponentProps<
|
| 42 |
+
typeof RechartsPrimitive.ResponsiveContainer
|
| 43 |
+
>["children"]
|
| 44 |
+
}
|
| 45 |
+
>(({ id, className, children, config, ...props }, ref) => {
|
| 46 |
+
const uniqueId = React.useId()
|
| 47 |
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
<ChartContext.Provider value={{ config }}>
|
| 51 |
+
<div
|
| 52 |
+
data-chart={chartId}
|
| 53 |
+
ref={ref}
|
| 54 |
+
className={cn(
|
| 55 |
+
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
| 56 |
+
className
|
| 57 |
+
)}
|
| 58 |
+
{...props}
|
| 59 |
+
>
|
| 60 |
+
<ChartStyle id={chartId} config={config} />
|
| 61 |
+
<RechartsPrimitive.ResponsiveContainer>
|
| 62 |
+
{children}
|
| 63 |
+
</RechartsPrimitive.ResponsiveContainer>
|
| 64 |
+
</div>
|
| 65 |
+
</ChartContext.Provider>
|
| 66 |
+
)
|
| 67 |
+
})
|
| 68 |
+
ChartContainer.displayName = "Chart"
|
| 69 |
+
|
| 70 |
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
| 71 |
+
const colorConfig = Object.entries(config).filter(
|
| 72 |
+
([, config]) => config.theme || config.color
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
if (!colorConfig.length) {
|
| 76 |
+
return null
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return (
|
| 80 |
+
<style
|
| 81 |
+
dangerouslySetInnerHTML={{
|
| 82 |
+
__html: Object.entries(THEMES)
|
| 83 |
+
.map(
|
| 84 |
+
([theme, prefix]) => `
|
| 85 |
+
${prefix} [data-chart=${id}] {
|
| 86 |
+
${colorConfig
|
| 87 |
+
.map(([key, itemConfig]) => {
|
| 88 |
+
const color =
|
| 89 |
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
| 90 |
+
itemConfig.color
|
| 91 |
+
return color ? ` --color-${key}: ${color};` : null
|
| 92 |
+
})
|
| 93 |
+
.join("\n")}
|
| 94 |
+
}
|
| 95 |
+
`
|
| 96 |
+
)
|
| 97 |
+
.join("\n"),
|
| 98 |
+
}}
|
| 99 |
+
/>
|
| 100 |
+
)
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const ChartTooltip = RechartsPrimitive.Tooltip
|
| 104 |
+
|
| 105 |
+
const ChartTooltipContent = React.forwardRef<
|
| 106 |
+
HTMLDivElement,
|
| 107 |
+
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
| 108 |
+
React.ComponentProps<"div"> & {
|
| 109 |
+
hideLabel?: boolean
|
| 110 |
+
hideIndicator?: boolean
|
| 111 |
+
indicator?: "line" | "dot" | "dashed"
|
| 112 |
+
nameKey?: string
|
| 113 |
+
labelKey?: string
|
| 114 |
+
}
|
| 115 |
+
>(
|
| 116 |
+
(
|
| 117 |
+
{
|
| 118 |
+
active,
|
| 119 |
+
payload,
|
| 120 |
+
className,
|
| 121 |
+
indicator = "dot",
|
| 122 |
+
hideLabel = false,
|
| 123 |
+
hideIndicator = false,
|
| 124 |
+
label,
|
| 125 |
+
labelFormatter,
|
| 126 |
+
labelClassName,
|
| 127 |
+
formatter,
|
| 128 |
+
color,
|
| 129 |
+
nameKey,
|
| 130 |
+
labelKey,
|
| 131 |
+
},
|
| 132 |
+
ref
|
| 133 |
+
) => {
|
| 134 |
+
const { config } = useChart()
|
| 135 |
+
|
| 136 |
+
const tooltipLabel = React.useMemo(() => {
|
| 137 |
+
if (hideLabel || !payload?.length) {
|
| 138 |
+
return null
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const [item] = payload
|
| 142 |
+
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
| 143 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
| 144 |
+
const value =
|
| 145 |
+
!labelKey && typeof label === "string"
|
| 146 |
+
? config[label as keyof typeof config]?.label || label
|
| 147 |
+
: itemConfig?.label
|
| 148 |
+
|
| 149 |
+
if (labelFormatter) {
|
| 150 |
+
return (
|
| 151 |
+
<div className={cn("font-medium", labelClassName)}>
|
| 152 |
+
{labelFormatter(value, payload)}
|
| 153 |
+
</div>
|
| 154 |
+
)
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
if (!value) {
|
| 158 |
+
return null
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
| 162 |
+
}, [
|
| 163 |
+
label,
|
| 164 |
+
labelFormatter,
|
| 165 |
+
payload,
|
| 166 |
+
hideLabel,
|
| 167 |
+
labelClassName,
|
| 168 |
+
config,
|
| 169 |
+
labelKey,
|
| 170 |
+
])
|
| 171 |
+
|
| 172 |
+
if (!active || !payload?.length) {
|
| 173 |
+
return null
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
const nestLabel = payload.length === 1 && indicator !== "dot"
|
| 177 |
+
|
| 178 |
+
return (
|
| 179 |
+
<div
|
| 180 |
+
ref={ref}
|
| 181 |
+
className={cn(
|
| 182 |
+
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
| 183 |
+
className
|
| 184 |
+
)}
|
| 185 |
+
>
|
| 186 |
+
{!nestLabel ? tooltipLabel : null}
|
| 187 |
+
<div className="grid gap-1.5">
|
| 188 |
+
{payload.map((item, index) => {
|
| 189 |
+
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
| 190 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
| 191 |
+
const indicatorColor = color || item.payload.fill || item.color
|
| 192 |
+
|
| 193 |
+
return (
|
| 194 |
+
<div
|
| 195 |
+
key={item.dataKey}
|
| 196 |
+
className={cn(
|
| 197 |
+
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
| 198 |
+
indicator === "dot" && "items-center"
|
| 199 |
+
)}
|
| 200 |
+
>
|
| 201 |
+
{formatter && item?.value !== undefined && item.name ? (
|
| 202 |
+
formatter(item.value, item.name, item, index, item.payload)
|
| 203 |
+
) : (
|
| 204 |
+
<>
|
| 205 |
+
{itemConfig?.icon ? (
|
| 206 |
+
<itemConfig.icon />
|
| 207 |
+
) : (
|
| 208 |
+
!hideIndicator && (
|
| 209 |
+
<div
|
| 210 |
+
className={cn(
|
| 211 |
+
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
| 212 |
+
{
|
| 213 |
+
"h-2.5 w-2.5": indicator === "dot",
|
| 214 |
+
"w-1": indicator === "line",
|
| 215 |
+
"w-0 border-[1.5px] border-dashed bg-transparent":
|
| 216 |
+
indicator === "dashed",
|
| 217 |
+
"my-0.5": nestLabel && indicator === "dashed",
|
| 218 |
+
}
|
| 219 |
+
)}
|
| 220 |
+
style={
|
| 221 |
+
{
|
| 222 |
+
"--color-bg": indicatorColor,
|
| 223 |
+
"--color-border": indicatorColor,
|
| 224 |
+
} as React.CSSProperties
|
| 225 |
+
}
|
| 226 |
+
/>
|
| 227 |
+
)
|
| 228 |
+
)}
|
| 229 |
+
<div
|
| 230 |
+
className={cn(
|
| 231 |
+
"flex flex-1 justify-between leading-none",
|
| 232 |
+
nestLabel ? "items-end" : "items-center"
|
| 233 |
+
)}
|
| 234 |
+
>
|
| 235 |
+
<div className="grid gap-1.5">
|
| 236 |
+
{nestLabel ? tooltipLabel : null}
|
| 237 |
+
<span className="text-muted-foreground">
|
| 238 |
+
{itemConfig?.label || item.name}
|
| 239 |
+
</span>
|
| 240 |
+
</div>
|
| 241 |
+
{item.value && (
|
| 242 |
+
<span className="font-mono font-medium tabular-nums text-foreground">
|
| 243 |
+
{item.value.toLocaleString()}
|
| 244 |
+
</span>
|
| 245 |
+
)}
|
| 246 |
+
</div>
|
| 247 |
+
</>
|
| 248 |
+
)}
|
| 249 |
+
</div>
|
| 250 |
+
)
|
| 251 |
+
})}
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
)
|
| 255 |
+
}
|
| 256 |
+
)
|
| 257 |
+
ChartTooltipContent.displayName = "ChartTooltip"
|
| 258 |
+
|
| 259 |
+
const ChartLegend = RechartsPrimitive.Legend
|
| 260 |
+
|
| 261 |
+
const ChartLegendContent = React.forwardRef<
|
| 262 |
+
HTMLDivElement,
|
| 263 |
+
React.ComponentProps<"div"> &
|
| 264 |
+
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
| 265 |
+
hideIcon?: boolean
|
| 266 |
+
nameKey?: string
|
| 267 |
+
}
|
| 268 |
+
>(
|
| 269 |
+
(
|
| 270 |
+
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
| 271 |
+
ref
|
| 272 |
+
) => {
|
| 273 |
+
const { config } = useChart()
|
| 274 |
+
|
| 275 |
+
if (!payload?.length) {
|
| 276 |
+
return null
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
return (
|
| 280 |
+
<div
|
| 281 |
+
ref={ref}
|
| 282 |
+
className={cn(
|
| 283 |
+
"flex items-center justify-center gap-4",
|
| 284 |
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
| 285 |
+
className
|
| 286 |
+
)}
|
| 287 |
+
>
|
| 288 |
+
{payload.map((item) => {
|
| 289 |
+
const key = `${nameKey || item.dataKey || "value"}`
|
| 290 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
| 291 |
+
|
| 292 |
+
return (
|
| 293 |
+
<div
|
| 294 |
+
key={item.value}
|
| 295 |
+
className={cn(
|
| 296 |
+
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
| 297 |
+
)}
|
| 298 |
+
>
|
| 299 |
+
{itemConfig?.icon && !hideIcon ? (
|
| 300 |
+
<itemConfig.icon />
|
| 301 |
+
) : (
|
| 302 |
+
<div
|
| 303 |
+
className="h-2 w-2 shrink-0 rounded-[2px]"
|
| 304 |
+
style={{
|
| 305 |
+
backgroundColor: item.color,
|
| 306 |
+
}}
|
| 307 |
+
/>
|
| 308 |
+
)}
|
| 309 |
+
{itemConfig?.label}
|
| 310 |
+
</div>
|
| 311 |
+
)
|
| 312 |
+
})}
|
| 313 |
+
</div>
|
| 314 |
+
)
|
| 315 |
+
}
|
| 316 |
+
)
|
| 317 |
+
ChartLegendContent.displayName = "ChartLegend"
|
| 318 |
+
|
| 319 |
+
// Helper to extract item config from a payload.
|
| 320 |
+
function getPayloadConfigFromPayload(
|
| 321 |
+
config: ChartConfig,
|
| 322 |
+
payload: unknown,
|
| 323 |
+
key: string
|
| 324 |
+
) {
|
| 325 |
+
if (typeof payload !== "object" || payload === null) {
|
| 326 |
+
return undefined
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
const payloadPayload =
|
| 330 |
+
"payload" in payload &&
|
| 331 |
+
typeof payload.payload === "object" &&
|
| 332 |
+
payload.payload !== null
|
| 333 |
+
? payload.payload
|
| 334 |
+
: undefined
|
| 335 |
+
|
| 336 |
+
let configLabelKey: string = key
|
| 337 |
+
|
| 338 |
+
if (
|
| 339 |
+
key in payload &&
|
| 340 |
+
typeof payload[key as keyof typeof payload] === "string"
|
| 341 |
+
) {
|
| 342 |
+
configLabelKey = payload[key as keyof typeof payload] as string
|
| 343 |
+
} else if (
|
| 344 |
+
payloadPayload &&
|
| 345 |
+
key in payloadPayload &&
|
| 346 |
+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
| 347 |
+
) {
|
| 348 |
+
configLabelKey = payloadPayload[
|
| 349 |
+
key as keyof typeof payloadPayload
|
| 350 |
+
] as string
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
return configLabelKey in config
|
| 354 |
+
? config[configLabelKey]
|
| 355 |
+
: config[key as keyof typeof config]
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
export {
|
| 359 |
+
ChartContainer,
|
| 360 |
+
ChartTooltip,
|
| 361 |
+
ChartTooltipContent,
|
| 362 |
+
ChartLegend,
|
| 363 |
+
ChartLegendContent,
|
| 364 |
+
ChartStyle,
|
| 365 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/checkbox.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
| 3 |
+
import { Check } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Checkbox = React.forwardRef<
|
| 8 |
+
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
| 9 |
+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
| 10 |
+
>(({ className, ...props }, ref) => (
|
| 11 |
+
<CheckboxPrimitive.Root
|
| 12 |
+
ref={ref}
|
| 13 |
+
className={cn(
|
| 14 |
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
| 15 |
+
className
|
| 16 |
+
)}
|
| 17 |
+
{...props}
|
| 18 |
+
>
|
| 19 |
+
<CheckboxPrimitive.Indicator
|
| 20 |
+
className={cn("flex items-center justify-center text-current")}
|
| 21 |
+
>
|
| 22 |
+
<Check className="h-4 w-4" />
|
| 23 |
+
</CheckboxPrimitive.Indicator>
|
| 24 |
+
</CheckboxPrimitive.Root>
|
| 25 |
+
))
|
| 26 |
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
| 27 |
+
|
| 28 |
+
export { Checkbox }
|
OpenAIChatAssistant/client/src/components/ui/collapsible.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
| 4 |
+
|
| 5 |
+
const Collapsible = CollapsiblePrimitive.Root
|
| 6 |
+
|
| 7 |
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
| 8 |
+
|
| 9 |
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
| 10 |
+
|
| 11 |
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
OpenAIChatAssistant/client/src/components/ui/command.tsx
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { type DialogProps } from "@radix-ui/react-dialog"
|
| 3 |
+
import { Command as CommandPrimitive } from "cmdk"
|
| 4 |
+
import { Search } from "lucide-react"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
| 8 |
+
|
| 9 |
+
const Command = React.forwardRef<
|
| 10 |
+
React.ElementRef<typeof CommandPrimitive>,
|
| 11 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
| 12 |
+
>(({ className, ...props }, ref) => (
|
| 13 |
+
<CommandPrimitive
|
| 14 |
+
ref={ref}
|
| 15 |
+
className={cn(
|
| 16 |
+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props}
|
| 20 |
+
/>
|
| 21 |
+
))
|
| 22 |
+
Command.displayName = CommandPrimitive.displayName
|
| 23 |
+
|
| 24 |
+
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
| 25 |
+
return (
|
| 26 |
+
<Dialog {...props}>
|
| 27 |
+
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
| 28 |
+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
| 29 |
+
{children}
|
| 30 |
+
</Command>
|
| 31 |
+
</DialogContent>
|
| 32 |
+
</Dialog>
|
| 33 |
+
)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const CommandInput = React.forwardRef<
|
| 37 |
+
React.ElementRef<typeof CommandPrimitive.Input>,
|
| 38 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
| 39 |
+
>(({ className, ...props }, ref) => (
|
| 40 |
+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
| 41 |
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
| 42 |
+
<CommandPrimitive.Input
|
| 43 |
+
ref={ref}
|
| 44 |
+
className={cn(
|
| 45 |
+
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
| 46 |
+
className
|
| 47 |
+
)}
|
| 48 |
+
{...props}
|
| 49 |
+
/>
|
| 50 |
+
</div>
|
| 51 |
+
))
|
| 52 |
+
|
| 53 |
+
CommandInput.displayName = CommandPrimitive.Input.displayName
|
| 54 |
+
|
| 55 |
+
const CommandList = React.forwardRef<
|
| 56 |
+
React.ElementRef<typeof CommandPrimitive.List>,
|
| 57 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
| 58 |
+
>(({ className, ...props }, ref) => (
|
| 59 |
+
<CommandPrimitive.List
|
| 60 |
+
ref={ref}
|
| 61 |
+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
| 62 |
+
{...props}
|
| 63 |
+
/>
|
| 64 |
+
))
|
| 65 |
+
|
| 66 |
+
CommandList.displayName = CommandPrimitive.List.displayName
|
| 67 |
+
|
| 68 |
+
const CommandEmpty = React.forwardRef<
|
| 69 |
+
React.ElementRef<typeof CommandPrimitive.Empty>,
|
| 70 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
| 71 |
+
>((props, ref) => (
|
| 72 |
+
<CommandPrimitive.Empty
|
| 73 |
+
ref={ref}
|
| 74 |
+
className="py-6 text-center text-sm"
|
| 75 |
+
{...props}
|
| 76 |
+
/>
|
| 77 |
+
))
|
| 78 |
+
|
| 79 |
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
| 80 |
+
|
| 81 |
+
const CommandGroup = React.forwardRef<
|
| 82 |
+
React.ElementRef<typeof CommandPrimitive.Group>,
|
| 83 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
| 84 |
+
>(({ className, ...props }, ref) => (
|
| 85 |
+
<CommandPrimitive.Group
|
| 86 |
+
ref={ref}
|
| 87 |
+
className={cn(
|
| 88 |
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
| 89 |
+
className
|
| 90 |
+
)}
|
| 91 |
+
{...props}
|
| 92 |
+
/>
|
| 93 |
+
))
|
| 94 |
+
|
| 95 |
+
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
| 96 |
+
|
| 97 |
+
const CommandSeparator = React.forwardRef<
|
| 98 |
+
React.ElementRef<typeof CommandPrimitive.Separator>,
|
| 99 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
| 100 |
+
>(({ className, ...props }, ref) => (
|
| 101 |
+
<CommandPrimitive.Separator
|
| 102 |
+
ref={ref}
|
| 103 |
+
className={cn("-mx-1 h-px bg-border", className)}
|
| 104 |
+
{...props}
|
| 105 |
+
/>
|
| 106 |
+
))
|
| 107 |
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
| 108 |
+
|
| 109 |
+
const CommandItem = React.forwardRef<
|
| 110 |
+
React.ElementRef<typeof CommandPrimitive.Item>,
|
| 111 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
| 112 |
+
>(({ className, ...props }, ref) => (
|
| 113 |
+
<CommandPrimitive.Item
|
| 114 |
+
ref={ref}
|
| 115 |
+
className={cn(
|
| 116 |
+
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 117 |
+
className
|
| 118 |
+
)}
|
| 119 |
+
{...props}
|
| 120 |
+
/>
|
| 121 |
+
))
|
| 122 |
+
|
| 123 |
+
CommandItem.displayName = CommandPrimitive.Item.displayName
|
| 124 |
+
|
| 125 |
+
const CommandShortcut = ({
|
| 126 |
+
className,
|
| 127 |
+
...props
|
| 128 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
| 129 |
+
return (
|
| 130 |
+
<span
|
| 131 |
+
className={cn(
|
| 132 |
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
| 133 |
+
className
|
| 134 |
+
)}
|
| 135 |
+
{...props}
|
| 136 |
+
/>
|
| 137 |
+
)
|
| 138 |
+
}
|
| 139 |
+
CommandShortcut.displayName = "CommandShortcut"
|
| 140 |
+
|
| 141 |
+
export {
|
| 142 |
+
Command,
|
| 143 |
+
CommandDialog,
|
| 144 |
+
CommandInput,
|
| 145 |
+
CommandList,
|
| 146 |
+
CommandEmpty,
|
| 147 |
+
CommandGroup,
|
| 148 |
+
CommandItem,
|
| 149 |
+
CommandShortcut,
|
| 150 |
+
CommandSeparator,
|
| 151 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/context-menu.tsx
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
| 3 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const ContextMenu = ContextMenuPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const ContextMenuGroup = ContextMenuPrimitive.Group
|
| 12 |
+
|
| 13 |
+
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
| 14 |
+
|
| 15 |
+
const ContextMenuSub = ContextMenuPrimitive.Sub
|
| 16 |
+
|
| 17 |
+
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
| 18 |
+
|
| 19 |
+
const ContextMenuSubTrigger = React.forwardRef<
|
| 20 |
+
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
| 21 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
| 22 |
+
inset?: boolean
|
| 23 |
+
}
|
| 24 |
+
>(({ className, inset, children, ...props }, ref) => (
|
| 25 |
+
<ContextMenuPrimitive.SubTrigger
|
| 26 |
+
ref={ref}
|
| 27 |
+
className={cn(
|
| 28 |
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
| 29 |
+
inset && "pl-8",
|
| 30 |
+
className
|
| 31 |
+
)}
|
| 32 |
+
{...props}
|
| 33 |
+
>
|
| 34 |
+
{children}
|
| 35 |
+
<ChevronRight className="ml-auto h-4 w-4" />
|
| 36 |
+
</ContextMenuPrimitive.SubTrigger>
|
| 37 |
+
))
|
| 38 |
+
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
| 39 |
+
|
| 40 |
+
const ContextMenuSubContent = React.forwardRef<
|
| 41 |
+
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
| 42 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
| 43 |
+
>(({ className, ...props }, ref) => (
|
| 44 |
+
<ContextMenuPrimitive.SubContent
|
| 45 |
+
ref={ref}
|
| 46 |
+
className={cn(
|
| 47 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
| 48 |
+
className
|
| 49 |
+
)}
|
| 50 |
+
{...props}
|
| 51 |
+
/>
|
| 52 |
+
))
|
| 53 |
+
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
| 54 |
+
|
| 55 |
+
const ContextMenuContent = React.forwardRef<
|
| 56 |
+
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
| 57 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
| 58 |
+
>(({ className, ...props }, ref) => (
|
| 59 |
+
<ContextMenuPrimitive.Portal>
|
| 60 |
+
<ContextMenuPrimitive.Content
|
| 61 |
+
ref={ref}
|
| 62 |
+
className={cn(
|
| 63 |
+
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
| 64 |
+
className
|
| 65 |
+
)}
|
| 66 |
+
{...props}
|
| 67 |
+
/>
|
| 68 |
+
</ContextMenuPrimitive.Portal>
|
| 69 |
+
))
|
| 70 |
+
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
| 71 |
+
|
| 72 |
+
const ContextMenuItem = React.forwardRef<
|
| 73 |
+
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
| 74 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
| 75 |
+
inset?: boolean
|
| 76 |
+
}
|
| 77 |
+
>(({ className, inset, ...props }, ref) => (
|
| 78 |
+
<ContextMenuPrimitive.Item
|
| 79 |
+
ref={ref}
|
| 80 |
+
className={cn(
|
| 81 |
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 82 |
+
inset && "pl-8",
|
| 83 |
+
className
|
| 84 |
+
)}
|
| 85 |
+
{...props}
|
| 86 |
+
/>
|
| 87 |
+
))
|
| 88 |
+
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
| 89 |
+
|
| 90 |
+
const ContextMenuCheckboxItem = React.forwardRef<
|
| 91 |
+
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
| 92 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
| 93 |
+
>(({ className, children, checked, ...props }, ref) => (
|
| 94 |
+
<ContextMenuPrimitive.CheckboxItem
|
| 95 |
+
ref={ref}
|
| 96 |
+
className={cn(
|
| 97 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 98 |
+
className
|
| 99 |
+
)}
|
| 100 |
+
checked={checked}
|
| 101 |
+
{...props}
|
| 102 |
+
>
|
| 103 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 104 |
+
<ContextMenuPrimitive.ItemIndicator>
|
| 105 |
+
<Check className="h-4 w-4" />
|
| 106 |
+
</ContextMenuPrimitive.ItemIndicator>
|
| 107 |
+
</span>
|
| 108 |
+
{children}
|
| 109 |
+
</ContextMenuPrimitive.CheckboxItem>
|
| 110 |
+
))
|
| 111 |
+
ContextMenuCheckboxItem.displayName =
|
| 112 |
+
ContextMenuPrimitive.CheckboxItem.displayName
|
| 113 |
+
|
| 114 |
+
const ContextMenuRadioItem = React.forwardRef<
|
| 115 |
+
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
| 116 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
| 117 |
+
>(({ className, children, ...props }, ref) => (
|
| 118 |
+
<ContextMenuPrimitive.RadioItem
|
| 119 |
+
ref={ref}
|
| 120 |
+
className={cn(
|
| 121 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 122 |
+
className
|
| 123 |
+
)}
|
| 124 |
+
{...props}
|
| 125 |
+
>
|
| 126 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 127 |
+
<ContextMenuPrimitive.ItemIndicator>
|
| 128 |
+
<Circle className="h-2 w-2 fill-current" />
|
| 129 |
+
</ContextMenuPrimitive.ItemIndicator>
|
| 130 |
+
</span>
|
| 131 |
+
{children}
|
| 132 |
+
</ContextMenuPrimitive.RadioItem>
|
| 133 |
+
))
|
| 134 |
+
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
| 135 |
+
|
| 136 |
+
const ContextMenuLabel = React.forwardRef<
|
| 137 |
+
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
| 138 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
| 139 |
+
inset?: boolean
|
| 140 |
+
}
|
| 141 |
+
>(({ className, inset, ...props }, ref) => (
|
| 142 |
+
<ContextMenuPrimitive.Label
|
| 143 |
+
ref={ref}
|
| 144 |
+
className={cn(
|
| 145 |
+
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
| 146 |
+
inset && "pl-8",
|
| 147 |
+
className
|
| 148 |
+
)}
|
| 149 |
+
{...props}
|
| 150 |
+
/>
|
| 151 |
+
))
|
| 152 |
+
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
| 153 |
+
|
| 154 |
+
const ContextMenuSeparator = React.forwardRef<
|
| 155 |
+
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
| 156 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
| 157 |
+
>(({ className, ...props }, ref) => (
|
| 158 |
+
<ContextMenuPrimitive.Separator
|
| 159 |
+
ref={ref}
|
| 160 |
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
| 161 |
+
{...props}
|
| 162 |
+
/>
|
| 163 |
+
))
|
| 164 |
+
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
| 165 |
+
|
| 166 |
+
const ContextMenuShortcut = ({
|
| 167 |
+
className,
|
| 168 |
+
...props
|
| 169 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
| 170 |
+
return (
|
| 171 |
+
<span
|
| 172 |
+
className={cn(
|
| 173 |
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
| 174 |
+
className
|
| 175 |
+
)}
|
| 176 |
+
{...props}
|
| 177 |
+
/>
|
| 178 |
+
)
|
| 179 |
+
}
|
| 180 |
+
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
| 181 |
+
|
| 182 |
+
export {
|
| 183 |
+
ContextMenu,
|
| 184 |
+
ContextMenuTrigger,
|
| 185 |
+
ContextMenuContent,
|
| 186 |
+
ContextMenuItem,
|
| 187 |
+
ContextMenuCheckboxItem,
|
| 188 |
+
ContextMenuRadioItem,
|
| 189 |
+
ContextMenuLabel,
|
| 190 |
+
ContextMenuSeparator,
|
| 191 |
+
ContextMenuShortcut,
|
| 192 |
+
ContextMenuGroup,
|
| 193 |
+
ContextMenuPortal,
|
| 194 |
+
ContextMenuSub,
|
| 195 |
+
ContextMenuSubContent,
|
| 196 |
+
ContextMenuSubTrigger,
|
| 197 |
+
ContextMenuRadioGroup,
|
| 198 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/dialog.tsx
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
| 3 |
+
import { X } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Dialog = DialogPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const DialogTrigger = DialogPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const DialogPortal = DialogPrimitive.Portal
|
| 12 |
+
|
| 13 |
+
const DialogClose = DialogPrimitive.Close
|
| 14 |
+
|
| 15 |
+
const DialogOverlay = React.forwardRef<
|
| 16 |
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
| 17 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
| 18 |
+
>(({ className, ...props }, ref) => (
|
| 19 |
+
<DialogPrimitive.Overlay
|
| 20 |
+
ref={ref}
|
| 21 |
+
className={cn(
|
| 22 |
+
"fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
| 23 |
+
className
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
/>
|
| 27 |
+
))
|
| 28 |
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
| 29 |
+
|
| 30 |
+
const DialogContent = React.forwardRef<
|
| 31 |
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
| 32 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
| 33 |
+
>(({ className, children, ...props }, ref) => (
|
| 34 |
+
<DialogPortal>
|
| 35 |
+
<DialogOverlay />
|
| 36 |
+
<DialogPrimitive.Content
|
| 37 |
+
ref={ref}
|
| 38 |
+
className={cn(
|
| 39 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg max-h-[85vh] translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 overflow-y-auto data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
| 40 |
+
className
|
| 41 |
+
)}
|
| 42 |
+
{...props}
|
| 43 |
+
>
|
| 44 |
+
{children}
|
| 45 |
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
| 46 |
+
<X className="h-4 w-4" />
|
| 47 |
+
<span className="sr-only">Close</span>
|
| 48 |
+
</DialogPrimitive.Close>
|
| 49 |
+
</DialogPrimitive.Content>
|
| 50 |
+
</DialogPortal>
|
| 51 |
+
))
|
| 52 |
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
| 53 |
+
|
| 54 |
+
const DialogHeader = ({
|
| 55 |
+
className,
|
| 56 |
+
...props
|
| 57 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 58 |
+
<div
|
| 59 |
+
className={cn(
|
| 60 |
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
| 61 |
+
className
|
| 62 |
+
)}
|
| 63 |
+
{...props}
|
| 64 |
+
/>
|
| 65 |
+
)
|
| 66 |
+
DialogHeader.displayName = "DialogHeader"
|
| 67 |
+
|
| 68 |
+
const DialogFooter = ({
|
| 69 |
+
className,
|
| 70 |
+
...props
|
| 71 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 72 |
+
<div
|
| 73 |
+
className={cn(
|
| 74 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
| 75 |
+
className
|
| 76 |
+
)}
|
| 77 |
+
{...props}
|
| 78 |
+
/>
|
| 79 |
+
)
|
| 80 |
+
DialogFooter.displayName = "DialogFooter"
|
| 81 |
+
|
| 82 |
+
const DialogTitle = React.forwardRef<
|
| 83 |
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
| 84 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
| 85 |
+
>(({ className, ...props }, ref) => (
|
| 86 |
+
<DialogPrimitive.Title
|
| 87 |
+
ref={ref}
|
| 88 |
+
className={cn(
|
| 89 |
+
"text-lg font-semibold leading-none tracking-tight",
|
| 90 |
+
className
|
| 91 |
+
)}
|
| 92 |
+
{...props}
|
| 93 |
+
/>
|
| 94 |
+
))
|
| 95 |
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
| 96 |
+
|
| 97 |
+
const DialogDescription = React.forwardRef<
|
| 98 |
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
| 99 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
| 100 |
+
>(({ className, ...props }, ref) => (
|
| 101 |
+
<DialogPrimitive.Description
|
| 102 |
+
ref={ref}
|
| 103 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 104 |
+
{...props}
|
| 105 |
+
/>
|
| 106 |
+
))
|
| 107 |
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
| 108 |
+
|
| 109 |
+
export {
|
| 110 |
+
Dialog,
|
| 111 |
+
DialogPortal,
|
| 112 |
+
DialogOverlay,
|
| 113 |
+
DialogClose,
|
| 114 |
+
DialogTrigger,
|
| 115 |
+
DialogContent,
|
| 116 |
+
DialogHeader,
|
| 117 |
+
DialogFooter,
|
| 118 |
+
DialogTitle,
|
| 119 |
+
DialogDescription,
|
| 120 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/drawer.tsx
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { Drawer as DrawerPrimitive } from "vaul"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
const Drawer = ({
|
| 9 |
+
shouldScaleBackground = true,
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
| 12 |
+
<DrawerPrimitive.Root
|
| 13 |
+
shouldScaleBackground={shouldScaleBackground}
|
| 14 |
+
{...props}
|
| 15 |
+
/>
|
| 16 |
+
)
|
| 17 |
+
Drawer.displayName = "Drawer"
|
| 18 |
+
|
| 19 |
+
const DrawerTrigger = DrawerPrimitive.Trigger
|
| 20 |
+
|
| 21 |
+
const DrawerPortal = DrawerPrimitive.Portal
|
| 22 |
+
|
| 23 |
+
const DrawerClose = DrawerPrimitive.Close
|
| 24 |
+
|
| 25 |
+
const DrawerOverlay = React.forwardRef<
|
| 26 |
+
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
| 27 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
| 28 |
+
>(({ className, ...props }, ref) => (
|
| 29 |
+
<DrawerPrimitive.Overlay
|
| 30 |
+
ref={ref}
|
| 31 |
+
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
| 32 |
+
{...props}
|
| 33 |
+
/>
|
| 34 |
+
))
|
| 35 |
+
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
| 36 |
+
|
| 37 |
+
const DrawerContent = React.forwardRef<
|
| 38 |
+
React.ElementRef<typeof DrawerPrimitive.Content>,
|
| 39 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
| 40 |
+
>(({ className, children, ...props }, ref) => (
|
| 41 |
+
<DrawerPortal>
|
| 42 |
+
<DrawerOverlay />
|
| 43 |
+
<DrawerPrimitive.Content
|
| 44 |
+
ref={ref}
|
| 45 |
+
className={cn(
|
| 46 |
+
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
| 47 |
+
className
|
| 48 |
+
)}
|
| 49 |
+
{...props}
|
| 50 |
+
>
|
| 51 |
+
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
| 52 |
+
{children}
|
| 53 |
+
</DrawerPrimitive.Content>
|
| 54 |
+
</DrawerPortal>
|
| 55 |
+
))
|
| 56 |
+
DrawerContent.displayName = "DrawerContent"
|
| 57 |
+
|
| 58 |
+
const DrawerHeader = ({
|
| 59 |
+
className,
|
| 60 |
+
...props
|
| 61 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 62 |
+
<div
|
| 63 |
+
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
| 64 |
+
{...props}
|
| 65 |
+
/>
|
| 66 |
+
)
|
| 67 |
+
DrawerHeader.displayName = "DrawerHeader"
|
| 68 |
+
|
| 69 |
+
const DrawerFooter = ({
|
| 70 |
+
className,
|
| 71 |
+
...props
|
| 72 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 73 |
+
<div
|
| 74 |
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
| 75 |
+
{...props}
|
| 76 |
+
/>
|
| 77 |
+
)
|
| 78 |
+
DrawerFooter.displayName = "DrawerFooter"
|
| 79 |
+
|
| 80 |
+
const DrawerTitle = React.forwardRef<
|
| 81 |
+
React.ElementRef<typeof DrawerPrimitive.Title>,
|
| 82 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
| 83 |
+
>(({ className, ...props }, ref) => (
|
| 84 |
+
<DrawerPrimitive.Title
|
| 85 |
+
ref={ref}
|
| 86 |
+
className={cn(
|
| 87 |
+
"text-lg font-semibold leading-none tracking-tight",
|
| 88 |
+
className
|
| 89 |
+
)}
|
| 90 |
+
{...props}
|
| 91 |
+
/>
|
| 92 |
+
))
|
| 93 |
+
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
| 94 |
+
|
| 95 |
+
const DrawerDescription = React.forwardRef<
|
| 96 |
+
React.ElementRef<typeof DrawerPrimitive.Description>,
|
| 97 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
| 98 |
+
>(({ className, ...props }, ref) => (
|
| 99 |
+
<DrawerPrimitive.Description
|
| 100 |
+
ref={ref}
|
| 101 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 102 |
+
{...props}
|
| 103 |
+
/>
|
| 104 |
+
))
|
| 105 |
+
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
| 106 |
+
|
| 107 |
+
export {
|
| 108 |
+
Drawer,
|
| 109 |
+
DrawerPortal,
|
| 110 |
+
DrawerOverlay,
|
| 111 |
+
DrawerTrigger,
|
| 112 |
+
DrawerClose,
|
| 113 |
+
DrawerContent,
|
| 114 |
+
DrawerHeader,
|
| 115 |
+
DrawerFooter,
|
| 116 |
+
DrawerTitle,
|
| 117 |
+
DrawerDescription,
|
| 118 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/dropdown-menu.tsx
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
| 3 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
| 12 |
+
|
| 13 |
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
| 14 |
+
|
| 15 |
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
| 16 |
+
|
| 17 |
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
| 18 |
+
|
| 19 |
+
const DropdownMenuSubTrigger = React.forwardRef<
|
| 20 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
| 21 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
| 22 |
+
inset?: boolean
|
| 23 |
+
}
|
| 24 |
+
>(({ className, inset, children, ...props }, ref) => (
|
| 25 |
+
<DropdownMenuPrimitive.SubTrigger
|
| 26 |
+
ref={ref}
|
| 27 |
+
className={cn(
|
| 28 |
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 29 |
+
inset && "pl-8",
|
| 30 |
+
className
|
| 31 |
+
)}
|
| 32 |
+
{...props}
|
| 33 |
+
>
|
| 34 |
+
{children}
|
| 35 |
+
<ChevronRight className="ml-auto" />
|
| 36 |
+
</DropdownMenuPrimitive.SubTrigger>
|
| 37 |
+
))
|
| 38 |
+
DropdownMenuSubTrigger.displayName =
|
| 39 |
+
DropdownMenuPrimitive.SubTrigger.displayName
|
| 40 |
+
|
| 41 |
+
const DropdownMenuSubContent = React.forwardRef<
|
| 42 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
| 43 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
| 44 |
+
>(({ className, ...props }, ref) => (
|
| 45 |
+
<DropdownMenuPrimitive.SubContent
|
| 46 |
+
ref={ref}
|
| 47 |
+
className={cn(
|
| 48 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
| 49 |
+
className
|
| 50 |
+
)}
|
| 51 |
+
{...props}
|
| 52 |
+
/>
|
| 53 |
+
))
|
| 54 |
+
DropdownMenuSubContent.displayName =
|
| 55 |
+
DropdownMenuPrimitive.SubContent.displayName
|
| 56 |
+
|
| 57 |
+
const DropdownMenuContent = React.forwardRef<
|
| 58 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
| 59 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
| 60 |
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
| 61 |
+
<DropdownMenuPrimitive.Portal>
|
| 62 |
+
<DropdownMenuPrimitive.Content
|
| 63 |
+
ref={ref}
|
| 64 |
+
sideOffset={sideOffset}
|
| 65 |
+
className={cn(
|
| 66 |
+
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
| 67 |
+
className
|
| 68 |
+
)}
|
| 69 |
+
{...props}
|
| 70 |
+
/>
|
| 71 |
+
</DropdownMenuPrimitive.Portal>
|
| 72 |
+
))
|
| 73 |
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
| 74 |
+
|
| 75 |
+
const DropdownMenuItem = React.forwardRef<
|
| 76 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
| 77 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
| 78 |
+
inset?: boolean
|
| 79 |
+
}
|
| 80 |
+
>(({ className, inset, ...props }, ref) => (
|
| 81 |
+
<DropdownMenuPrimitive.Item
|
| 82 |
+
ref={ref}
|
| 83 |
+
className={cn(
|
| 84 |
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 85 |
+
inset && "pl-8",
|
| 86 |
+
className
|
| 87 |
+
)}
|
| 88 |
+
{...props}
|
| 89 |
+
/>
|
| 90 |
+
))
|
| 91 |
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
| 92 |
+
|
| 93 |
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
| 94 |
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
| 95 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
| 96 |
+
>(({ className, children, checked, ...props }, ref) => (
|
| 97 |
+
<DropdownMenuPrimitive.CheckboxItem
|
| 98 |
+
ref={ref}
|
| 99 |
+
className={cn(
|
| 100 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 101 |
+
className
|
| 102 |
+
)}
|
| 103 |
+
checked={checked}
|
| 104 |
+
{...props}
|
| 105 |
+
>
|
| 106 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 107 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
| 108 |
+
<Check className="h-4 w-4" />
|
| 109 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
| 110 |
+
</span>
|
| 111 |
+
{children}
|
| 112 |
+
</DropdownMenuPrimitive.CheckboxItem>
|
| 113 |
+
))
|
| 114 |
+
DropdownMenuCheckboxItem.displayName =
|
| 115 |
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
| 116 |
+
|
| 117 |
+
const DropdownMenuRadioItem = React.forwardRef<
|
| 118 |
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
| 119 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
| 120 |
+
>(({ className, children, ...props }, ref) => (
|
| 121 |
+
<DropdownMenuPrimitive.RadioItem
|
| 122 |
+
ref={ref}
|
| 123 |
+
className={cn(
|
| 124 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 125 |
+
className
|
| 126 |
+
)}
|
| 127 |
+
{...props}
|
| 128 |
+
>
|
| 129 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 130 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
| 131 |
+
<Circle className="h-2 w-2 fill-current" />
|
| 132 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
| 133 |
+
</span>
|
| 134 |
+
{children}
|
| 135 |
+
</DropdownMenuPrimitive.RadioItem>
|
| 136 |
+
))
|
| 137 |
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
| 138 |
+
|
| 139 |
+
const DropdownMenuLabel = React.forwardRef<
|
| 140 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
| 141 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
| 142 |
+
inset?: boolean
|
| 143 |
+
}
|
| 144 |
+
>(({ className, inset, ...props }, ref) => (
|
| 145 |
+
<DropdownMenuPrimitive.Label
|
| 146 |
+
ref={ref}
|
| 147 |
+
className={cn(
|
| 148 |
+
"px-2 py-1.5 text-sm font-semibold",
|
| 149 |
+
inset && "pl-8",
|
| 150 |
+
className
|
| 151 |
+
)}
|
| 152 |
+
{...props}
|
| 153 |
+
/>
|
| 154 |
+
))
|
| 155 |
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
| 156 |
+
|
| 157 |
+
const DropdownMenuSeparator = React.forwardRef<
|
| 158 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
| 159 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
| 160 |
+
>(({ className, ...props }, ref) => (
|
| 161 |
+
<DropdownMenuPrimitive.Separator
|
| 162 |
+
ref={ref}
|
| 163 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 164 |
+
{...props}
|
| 165 |
+
/>
|
| 166 |
+
))
|
| 167 |
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
| 168 |
+
|
| 169 |
+
const DropdownMenuShortcut = ({
|
| 170 |
+
className,
|
| 171 |
+
...props
|
| 172 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
| 173 |
+
return (
|
| 174 |
+
<span
|
| 175 |
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
| 176 |
+
{...props}
|
| 177 |
+
/>
|
| 178 |
+
)
|
| 179 |
+
}
|
| 180 |
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
| 181 |
+
|
| 182 |
+
export {
|
| 183 |
+
DropdownMenu,
|
| 184 |
+
DropdownMenuTrigger,
|
| 185 |
+
DropdownMenuContent,
|
| 186 |
+
DropdownMenuItem,
|
| 187 |
+
DropdownMenuCheckboxItem,
|
| 188 |
+
DropdownMenuRadioItem,
|
| 189 |
+
DropdownMenuLabel,
|
| 190 |
+
DropdownMenuSeparator,
|
| 191 |
+
DropdownMenuShortcut,
|
| 192 |
+
DropdownMenuGroup,
|
| 193 |
+
DropdownMenuPortal,
|
| 194 |
+
DropdownMenuSub,
|
| 195 |
+
DropdownMenuSubContent,
|
| 196 |
+
DropdownMenuSubTrigger,
|
| 197 |
+
DropdownMenuRadioGroup,
|
| 198 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/form.tsx
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
| 3 |
+
import { Slot } from "@radix-ui/react-slot"
|
| 4 |
+
import {
|
| 5 |
+
Controller,
|
| 6 |
+
ControllerProps,
|
| 7 |
+
FieldPath,
|
| 8 |
+
FieldValues,
|
| 9 |
+
FormProvider,
|
| 10 |
+
useFormContext,
|
| 11 |
+
} from "react-hook-form"
|
| 12 |
+
|
| 13 |
+
import { cn } from "@/lib/utils"
|
| 14 |
+
import { Label } from "@/components/ui/label"
|
| 15 |
+
|
| 16 |
+
const Form = FormProvider
|
| 17 |
+
|
| 18 |
+
type FormFieldContextValue<
|
| 19 |
+
TFieldValues extends FieldValues = FieldValues,
|
| 20 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
| 21 |
+
> = {
|
| 22 |
+
name: TName
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
| 26 |
+
{} as FormFieldContextValue
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
const FormField = <
|
| 30 |
+
TFieldValues extends FieldValues = FieldValues,
|
| 31 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
| 32 |
+
>({
|
| 33 |
+
...props
|
| 34 |
+
}: ControllerProps<TFieldValues, TName>) => {
|
| 35 |
+
return (
|
| 36 |
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
| 37 |
+
<Controller {...props} />
|
| 38 |
+
</FormFieldContext.Provider>
|
| 39 |
+
)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const useFormField = () => {
|
| 43 |
+
const fieldContext = React.useContext(FormFieldContext)
|
| 44 |
+
const itemContext = React.useContext(FormItemContext)
|
| 45 |
+
const { getFieldState, formState } = useFormContext()
|
| 46 |
+
|
| 47 |
+
const fieldState = getFieldState(fieldContext.name, formState)
|
| 48 |
+
|
| 49 |
+
if (!fieldContext) {
|
| 50 |
+
throw new Error("useFormField should be used within <FormField>")
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const { id } = itemContext
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
id,
|
| 57 |
+
name: fieldContext.name,
|
| 58 |
+
formItemId: `${id}-form-item`,
|
| 59 |
+
formDescriptionId: `${id}-form-item-description`,
|
| 60 |
+
formMessageId: `${id}-form-item-message`,
|
| 61 |
+
...fieldState,
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
type FormItemContextValue = {
|
| 66 |
+
id: string
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
| 70 |
+
{} as FormItemContextValue
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
const FormItem = React.forwardRef<
|
| 74 |
+
HTMLDivElement,
|
| 75 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 76 |
+
>(({ className, ...props }, ref) => {
|
| 77 |
+
const id = React.useId()
|
| 78 |
+
|
| 79 |
+
return (
|
| 80 |
+
<FormItemContext.Provider value={{ id }}>
|
| 81 |
+
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
| 82 |
+
</FormItemContext.Provider>
|
| 83 |
+
)
|
| 84 |
+
})
|
| 85 |
+
FormItem.displayName = "FormItem"
|
| 86 |
+
|
| 87 |
+
const FormLabel = React.forwardRef<
|
| 88 |
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
| 89 |
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
| 90 |
+
>(({ className, ...props }, ref) => {
|
| 91 |
+
const { error, formItemId } = useFormField()
|
| 92 |
+
|
| 93 |
+
return (
|
| 94 |
+
<Label
|
| 95 |
+
ref={ref}
|
| 96 |
+
className={cn(error && "text-destructive", className)}
|
| 97 |
+
htmlFor={formItemId}
|
| 98 |
+
{...props}
|
| 99 |
+
/>
|
| 100 |
+
)
|
| 101 |
+
})
|
| 102 |
+
FormLabel.displayName = "FormLabel"
|
| 103 |
+
|
| 104 |
+
const FormControl = React.forwardRef<
|
| 105 |
+
React.ElementRef<typeof Slot>,
|
| 106 |
+
React.ComponentPropsWithoutRef<typeof Slot>
|
| 107 |
+
>(({ ...props }, ref) => {
|
| 108 |
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
| 109 |
+
|
| 110 |
+
return (
|
| 111 |
+
<Slot
|
| 112 |
+
ref={ref}
|
| 113 |
+
id={formItemId}
|
| 114 |
+
aria-describedby={
|
| 115 |
+
!error
|
| 116 |
+
? `${formDescriptionId}`
|
| 117 |
+
: `${formDescriptionId} ${formMessageId}`
|
| 118 |
+
}
|
| 119 |
+
aria-invalid={!!error}
|
| 120 |
+
{...props}
|
| 121 |
+
/>
|
| 122 |
+
)
|
| 123 |
+
})
|
| 124 |
+
FormControl.displayName = "FormControl"
|
| 125 |
+
|
| 126 |
+
const FormDescription = React.forwardRef<
|
| 127 |
+
HTMLParagraphElement,
|
| 128 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
| 129 |
+
>(({ className, ...props }, ref) => {
|
| 130 |
+
const { formDescriptionId } = useFormField()
|
| 131 |
+
|
| 132 |
+
return (
|
| 133 |
+
<p
|
| 134 |
+
ref={ref}
|
| 135 |
+
id={formDescriptionId}
|
| 136 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 137 |
+
{...props}
|
| 138 |
+
/>
|
| 139 |
+
)
|
| 140 |
+
})
|
| 141 |
+
FormDescription.displayName = "FormDescription"
|
| 142 |
+
|
| 143 |
+
const FormMessage = React.forwardRef<
|
| 144 |
+
HTMLParagraphElement,
|
| 145 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
| 146 |
+
>(({ className, children, ...props }, ref) => {
|
| 147 |
+
const { error, formMessageId } = useFormField()
|
| 148 |
+
const body = error ? String(error?.message) : children
|
| 149 |
+
|
| 150 |
+
if (!body) {
|
| 151 |
+
return null
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return (
|
| 155 |
+
<p
|
| 156 |
+
ref={ref}
|
| 157 |
+
id={formMessageId}
|
| 158 |
+
className={cn("text-sm font-medium text-destructive", className)}
|
| 159 |
+
{...props}
|
| 160 |
+
>
|
| 161 |
+
{body}
|
| 162 |
+
</p>
|
| 163 |
+
)
|
| 164 |
+
})
|
| 165 |
+
FormMessage.displayName = "FormMessage"
|
| 166 |
+
|
| 167 |
+
export {
|
| 168 |
+
useFormField,
|
| 169 |
+
Form,
|
| 170 |
+
FormItem,
|
| 171 |
+
FormLabel,
|
| 172 |
+
FormControl,
|
| 173 |
+
FormDescription,
|
| 174 |
+
FormMessage,
|
| 175 |
+
FormField,
|
| 176 |
+
}
|
OpenAIChatAssistant/client/src/components/ui/hover-card.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
const HoverCard = HoverCardPrimitive.Root
|
| 9 |
+
|
| 10 |
+
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
| 11 |
+
|
| 12 |
+
const HoverCardContent = React.forwardRef<
|
| 13 |
+
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
| 14 |
+
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
| 15 |
+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
| 16 |
+
<HoverCardPrimitive.Content
|
| 17 |
+
ref={ref}
|
| 18 |
+
align={align}
|
| 19 |
+
sideOffset={sideOffset}
|
| 20 |
+
className={cn(
|
| 21 |
+
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
| 22 |
+
className
|
| 23 |
+
)}
|
| 24 |
+
{...props}
|
| 25 |
+
/>
|
| 26 |
+
))
|
| 27 |
+
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
| 28 |
+
|
| 29 |
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|