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 }
|