Spaces:
Build error
Build error
Upload components directory - minimal build
Browse files- code-editor.tsx +146 -0
- conversation-panel.tsx +222 -0
- model-selector.tsx +165 -0
- providers.tsx +18 -0
- ui/badge.tsx +36 -0
- ui/button.tsx +56 -0
- ui/card.tsx +79 -0
- ui/select.tsx +158 -0
- ui/tabs.tsx +53 -0
- ui/textarea.tsx +24 -0
code-editor.tsx
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useRef } from 'react';
|
| 4 |
+
import { Editor } from '@monaco-editor/react';
|
| 5 |
+
import { cn } from '@/lib/utils';
|
| 6 |
+
|
| 7 |
+
interface CodeEditorProps {
|
| 8 |
+
value: string;
|
| 9 |
+
onChange: (value: string) => void;
|
| 10 |
+
className?: string;
|
| 11 |
+
language?: string;
|
| 12 |
+
theme?: string;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function CodeEditor({
|
| 16 |
+
value,
|
| 17 |
+
onChange,
|
| 18 |
+
className,
|
| 19 |
+
language = 'javascript',
|
| 20 |
+
theme = 'vs-dark'
|
| 21 |
+
}: CodeEditorProps) {
|
| 22 |
+
const editorRef = useRef<any>(null);
|
| 23 |
+
|
| 24 |
+
const handleEditorDidMount = (editor: any) => {
|
| 25 |
+
editorRef.current = editor;
|
| 26 |
+
|
| 27 |
+
// Configure editor options
|
| 28 |
+
editor.updateOptions({
|
| 29 |
+
fontSize: 14,
|
| 30 |
+
fontFamily: 'JetBrains Mono, Monaco, Consolas, "Courier New", monospace',
|
| 31 |
+
minimap: { enabled: false },
|
| 32 |
+
scrollBeyondLastLine: false,
|
| 33 |
+
wordWrap: 'on',
|
| 34 |
+
automaticLayout: true,
|
| 35 |
+
tabSize: 2,
|
| 36 |
+
insertSpaces: true,
|
| 37 |
+
roundedSelection: false,
|
| 38 |
+
readOnly: false,
|
| 39 |
+
cursorStyle: 'line',
|
| 40 |
+
lineNumbers: 'on',
|
| 41 |
+
renderLineHighlight: 'line',
|
| 42 |
+
selectOnLineNumbers: true,
|
| 43 |
+
roundedSelection: false,
|
| 44 |
+
renderIndentGuides: true,
|
| 45 |
+
colorDecorators: true,
|
| 46 |
+
lineDecorationsWidth: 0,
|
| 47 |
+
lineNumbersMinChars: 3,
|
| 48 |
+
folding: true,
|
| 49 |
+
foldingHighlight: true,
|
| 50 |
+
showFoldingControls: 'mouseover',
|
| 51 |
+
smoothScrolling: true,
|
| 52 |
+
contextmenu: true,
|
| 53 |
+
mouseWheelZoom: true,
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
// Add custom key bindings
|
| 57 |
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
| 58 |
+
// Handle save action
|
| 59 |
+
console.log('Save action triggered');
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
// Add code completion suggestions
|
| 63 |
+
const suggestions = [
|
| 64 |
+
{
|
| 65 |
+
label: 'console.log',
|
| 66 |
+
kind: monaco.languages.CompletionItemKind.Function,
|
| 67 |
+
insertText: 'console.log(${1:message});',
|
| 68 |
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
| 69 |
+
documentation: 'Log a message to the console'
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
label: 'function',
|
| 73 |
+
kind: monaco.languages.CompletionItemKind.Snippet,
|
| 74 |
+
insertText: 'function ${1:functionName}(${2:params}) {\n\t${3:// function body}\n}',
|
| 75 |
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
| 76 |
+
documentation: 'Create a function'
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
label: 'if',
|
| 80 |
+
kind: monaco.languages.CompletionItemKind.Snippet,
|
| 81 |
+
insertText: 'if (${1:condition}) {\n\t${2:// code}\n}',
|
| 82 |
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
| 83 |
+
documentation: 'Create an if statement'
|
| 84 |
+
}
|
| 85 |
+
];
|
| 86 |
+
|
| 87 |
+
// Register completion provider
|
| 88 |
+
const completionProvider = {
|
| 89 |
+
provideCompletionItems: () => ({
|
| 90 |
+
suggestions: suggestions.map(suggestion => ({
|
| 91 |
+
...suggestion,
|
| 92 |
+
range: undefined
|
| 93 |
+
}))
|
| 94 |
+
})
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
// Register the completion provider
|
| 98 |
+
const disposable = monaco.languages.registerCompletionItemProvider(language, completionProvider);
|
| 99 |
+
|
| 100 |
+
// Cleanup on unmount
|
| 101 |
+
return () => {
|
| 102 |
+
disposable.dispose();
|
| 103 |
+
};
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
const handleEditorChange = (value: string | undefined) => {
|
| 107 |
+
if (value !== undefined) {
|
| 108 |
+
onChange(value);
|
| 109 |
+
}
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
return (
|
| 113 |
+
<div className={cn("monaco-editor-container", className)}>
|
| 114 |
+
<Editor
|
| 115 |
+
height="100%"
|
| 116 |
+
defaultLanguage={language}
|
| 117 |
+
theme={theme}
|
| 118 |
+
value={value}
|
| 119 |
+
onChange={handleEditorChange}
|
| 120 |
+
onMount={handleEditorDidMount}
|
| 121 |
+
options={{
|
| 122 |
+
selectOnLineNumbers: true,
|
| 123 |
+
roundedSelection: false,
|
| 124 |
+
readOnly: false,
|
| 125 |
+
cursorStyle: 'line',
|
| 126 |
+
automaticLayout: true,
|
| 127 |
+
wordWrap: 'on',
|
| 128 |
+
minimap: { enabled: false },
|
| 129 |
+
scrollBeyondLastLine: false,
|
| 130 |
+
fontSize: 14,
|
| 131 |
+
fontFamily: 'JetBrains Mono, Monaco, Consolas, "Courier New", monospace',
|
| 132 |
+
tabSize: 2,
|
| 133 |
+
insertSpaces: true,
|
| 134 |
+
renderLineHighlight: 'line',
|
| 135 |
+
lineNumbers: 'on',
|
| 136 |
+
contextmenu: true,
|
| 137 |
+
mouseWheelZoom: true,
|
| 138 |
+
smoothScrolling: true,
|
| 139 |
+
folding: true,
|
| 140 |
+
foldingHighlight: true,
|
| 141 |
+
showFoldingControls: 'mouseover',
|
| 142 |
+
}}
|
| 143 |
+
/>
|
| 144 |
+
</div>
|
| 145 |
+
);
|
| 146 |
+
}
|
conversation-panel.tsx
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState, useEffect } from 'react';
|
| 4 |
+
import { Card, CardContent } from '@/components/ui/card';
|
| 5 |
+
import { Badge } from '@/components/ui/badge';
|
| 6 |
+
import { Button } from '@/components/ui/button';
|
| 7 |
+
import { MessageSquare, Copy, ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-react';
|
| 8 |
+
import { cn } from '@/lib/utils';
|
| 9 |
+
|
| 10 |
+
interface ConversationPanelProps {
|
| 11 |
+
completion: string;
|
| 12 |
+
isLoading: boolean;
|
| 13 |
+
className?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface Message {
|
| 17 |
+
id: string;
|
| 18 |
+
role: 'user' | 'assistant';
|
| 19 |
+
content: string;
|
| 20 |
+
timestamp: Date;
|
| 21 |
+
feedback?: 'positive' | 'negative';
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function ConversationPanel({ completion, isLoading, className }: ConversationPanelProps) {
|
| 25 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
| 26 |
+
const [currentMessage, setCurrentMessage] = useState<string>('');
|
| 27 |
+
|
| 28 |
+
// Update messages when completion changes
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
if (completion) {
|
| 31 |
+
const newMessage: Message = {
|
| 32 |
+
id: Date.now().toString(),
|
| 33 |
+
role: 'assistant',
|
| 34 |
+
content: completion,
|
| 35 |
+
timestamp: new Date(),
|
| 36 |
+
};
|
| 37 |
+
setMessages(prev => [...prev, newMessage]);
|
| 38 |
+
}
|
| 39 |
+
}, [completion]);
|
| 40 |
+
|
| 41 |
+
// Update current message during streaming
|
| 42 |
+
useEffect(() => {
|
| 43 |
+
if (isLoading && completion) {
|
| 44 |
+
setCurrentMessage(completion);
|
| 45 |
+
} else if (!isLoading) {
|
| 46 |
+
setCurrentMessage('');
|
| 47 |
+
}
|
| 48 |
+
}, [completion, isLoading]);
|
| 49 |
+
|
| 50 |
+
const handleCopy = (content: string) => {
|
| 51 |
+
navigator.clipboard.writeText(content);
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
const handleFeedback = (messageId: string, feedback: 'positive' | 'negative') => {
|
| 55 |
+
setMessages(prev => prev.map(msg =>
|
| 56 |
+
msg.id === messageId ? { ...msg, feedback } : msg
|
| 57 |
+
));
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const formatCode = (content: string) => {
|
| 61 |
+
// Simple code block formatting
|
| 62 |
+
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
| 63 |
+
let formatted = content;
|
| 64 |
+
let match;
|
| 65 |
+
|
| 66 |
+
const parts: Array<{ type: 'text' | 'code'; content: string; language?: string }> = [];
|
| 67 |
+
let lastIndex = 0;
|
| 68 |
+
|
| 69 |
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
| 70 |
+
if (match.index > lastIndex) {
|
| 71 |
+
parts.push({ type: 'text', content: content.slice(lastIndex, match.index) });
|
| 72 |
+
}
|
| 73 |
+
parts.push({
|
| 74 |
+
type: 'code',
|
| 75 |
+
content: match[2].trim(),
|
| 76 |
+
language: match[1] || 'javascript'
|
| 77 |
+
});
|
| 78 |
+
lastIndex = match.index + match[0].length;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if (lastIndex < content.length) {
|
| 82 |
+
parts.push({ type: 'text', content: content.slice(lastIndex) });
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
return parts.length > 0 ? parts : [{ type: 'text', content }];
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
return (
|
| 89 |
+
<div className={cn("flex flex-col h-full", className)}>
|
| 90 |
+
{/* Messages */}
|
| 91 |
+
<div className="flex-1 overflow-y-auto space-y-4 mb-4 max-h-[400px] code-area">
|
| 92 |
+
{messages.length === 0 && !isLoading && (
|
| 93 |
+
<div className="flex items-center justify-center h-full text-muted-foreground">
|
| 94 |
+
<div className="text-center">
|
| 95 |
+
<MessageSquare className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
| 96 |
+
<p className="text-lg font-medium mb-2">Start a conversation</p>
|
| 97 |
+
<p className="text-sm">
|
| 98 |
+
Ask me to help with your code, generate functions, debug issues, or discuss programming concepts.
|
| 99 |
+
</p>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
)}
|
| 103 |
+
|
| 104 |
+
{messages.map((message) => (
|
| 105 |
+
<Card key={message.id} className="relative">
|
| 106 |
+
<CardContent className="p-4">
|
| 107 |
+
<div className="flex items-start justify-between mb-3">
|
| 108 |
+
<div className="flex items-center gap-2">
|
| 109 |
+
<Badge
|
| 110 |
+
variant={message.role === 'assistant' ? 'default' : 'secondary'}
|
| 111 |
+
className="text-xs"
|
| 112 |
+
>
|
| 113 |
+
{message.role === 'assistant' ? 'AI Assistant' : 'You'}
|
| 114 |
+
</Badge>
|
| 115 |
+
<span className="text-xs text-muted-foreground">
|
| 116 |
+
{message.timestamp.toLocaleTimeString()}
|
| 117 |
+
</span>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<div className="flex items-center gap-1">
|
| 121 |
+
<Button
|
| 122 |
+
variant="ghost"
|
| 123 |
+
size="sm"
|
| 124 |
+
onClick={() => handleCopy(message.content)}
|
| 125 |
+
className="h-6 w-6 p-0"
|
| 126 |
+
>
|
| 127 |
+
<Copy className="w-3 h-3" />
|
| 128 |
+
</Button>
|
| 129 |
+
|
| 130 |
+
{message.role === 'assistant' && (
|
| 131 |
+
<>
|
| 132 |
+
<Button
|
| 133 |
+
variant="ghost"
|
| 134 |
+
size="sm"
|
| 135 |
+
onClick={() => handleFeedback(message.id, 'positive')}
|
| 136 |
+
className={cn(
|
| 137 |
+
"h-6 w-6 p-0",
|
| 138 |
+
message.feedback === 'positive' && "text-green-600"
|
| 139 |
+
)}
|
| 140 |
+
>
|
| 141 |
+
<ThumbsUp className="w-3 h-3" />
|
| 142 |
+
</Button>
|
| 143 |
+
|
| 144 |
+
<Button
|
| 145 |
+
variant="ghost"
|
| 146 |
+
size="sm"
|
| 147 |
+
onClick={() => handleFeedback(message.id, 'negative')}
|
| 148 |
+
className={cn(
|
| 149 |
+
"h-6 w-6 p-0",
|
| 150 |
+
message.feedback === 'negative' && "text-red-600"
|
| 151 |
+
)}
|
| 152 |
+
>
|
| 153 |
+
<ThumbsDown className="w-3 h-3" />
|
| 154 |
+
</Button>
|
| 155 |
+
</>
|
| 156 |
+
)}
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div className="prose prose-sm max-w-none">
|
| 161 |
+
{formatCode(message.content).map((part, index) => (
|
| 162 |
+
<div key={index}>
|
| 163 |
+
{part.type === 'code' ? (
|
| 164 |
+
<div className="relative">
|
| 165 |
+
<Badge variant="outline" className="text-xs mb-2">
|
| 166 |
+
{part.language}
|
| 167 |
+
</Badge>
|
| 168 |
+
<pre className="bg-muted p-3 rounded-md overflow-x-auto text-sm">
|
| 169 |
+
<code>{part.content}</code>
|
| 170 |
+
</pre>
|
| 171 |
+
</div>
|
| 172 |
+
) : (
|
| 173 |
+
<div className="whitespace-pre-wrap text-sm leading-relaxed">
|
| 174 |
+
{part.content}
|
| 175 |
+
</div>
|
| 176 |
+
)}
|
| 177 |
+
</div>
|
| 178 |
+
))}
|
| 179 |
+
</div>
|
| 180 |
+
</CardContent>
|
| 181 |
+
</Card>
|
| 182 |
+
))}
|
| 183 |
+
|
| 184 |
+
{/* Current streaming message */}
|
| 185 |
+
{isLoading && currentMessage && (
|
| 186 |
+
<Card className="relative border-dashed">
|
| 187 |
+
<CardContent className="p-4">
|
| 188 |
+
<div className="flex items-center gap-2 mb-3">
|
| 189 |
+
<Badge variant="default" className="text-xs">
|
| 190 |
+
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
| 191 |
+
AI Assistant
|
| 192 |
+
</Badge>
|
| 193 |
+
<span className="text-xs text-muted-foreground">Thinking...</span>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
<div className="prose prose-sm max-w-none">
|
| 197 |
+
{formatCode(currentMessage).map((part, index) => (
|
| 198 |
+
<div key={index}>
|
| 199 |
+
{part.type === 'code' ? (
|
| 200 |
+
<div className="relative">
|
| 201 |
+
<Badge variant="outline" className="text-xs mb-2">
|
| 202 |
+
{part.language}
|
| 203 |
+
</Badge>
|
| 204 |
+
<pre className="bg-muted p-3 rounded-md overflow-x-auto text-sm opacity-75">
|
| 205 |
+
<code>{part.content}</code>
|
| 206 |
+
</pre>
|
| 207 |
+
</div>
|
| 208 |
+
) : (
|
| 209 |
+
<div className="whitespace-pre-wrap text-sm leading-relaxed opacity-75">
|
| 210 |
+
{part.content}
|
| 211 |
+
</div>
|
| 212 |
+
)}
|
| 213 |
+
</div>
|
| 214 |
+
))}
|
| 215 |
+
</div>
|
| 216 |
+
</CardContent>
|
| 217 |
+
</Card>
|
| 218 |
+
)}
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
);
|
| 222 |
+
}
|
model-selector.tsx
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
| 5 |
+
import { Badge } from '@/components/ui/badge';
|
| 6 |
+
import { Settings, Cpu, Zap, Brain } from 'lucide-react';
|
| 7 |
+
|
| 8 |
+
interface ModelSelectorProps {
|
| 9 |
+
selectedModel: string;
|
| 10 |
+
onModelChange: (model: string) => void;
|
| 11 |
+
models: string[];
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const modelIcons: Record<string, React.ReactNode> = {
|
| 15 |
+
'deepseek-ai': <Brain className="w-3 h-3" />,
|
| 16 |
+
'Qwen': <Cpu className="w-3 h-3" />,
|
| 17 |
+
'moonshotai': <Zap className="w-3 h-3" />,
|
| 18 |
+
'zai-org': <Settings className="w-3 h-3" />,
|
| 19 |
+
'MiniMaxAI': <Brain className="w-3 h-3" />,
|
| 20 |
+
'meta-llama': <Cpu className="w-3 h-3" />,
|
| 21 |
+
'google': <Brain className="w-3 h-3" />,
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
const getProviderFromModel = (modelName: string): string => {
|
| 25 |
+
if (modelName.includes('deepseek-ai') || modelName.includes('Qwen') ||
|
| 26 |
+
modelName.includes('moonshotai') || modelName.includes('zai-org') ||
|
| 27 |
+
modelName.includes('MiniMaxAI') || modelName.includes('meta-llama') ||
|
| 28 |
+
modelName.includes('google')) {
|
| 29 |
+
return 'Hugging Face';
|
| 30 |
+
}
|
| 31 |
+
return 'Unknown';
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
const getModelCategory = (modelName: string): string => {
|
| 35 |
+
if (modelName.includes('DeepSeek')) return 'Reasoning';
|
| 36 |
+
if (modelName.includes('Coder')) return 'Code';
|
| 37 |
+
if (modelName.includes('VL')) return 'Vision';
|
| 38 |
+
if (modelName.includes('Kimi') && modelName.includes('Thinking')) return 'Reasoning';
|
| 39 |
+
if (modelName.includes('Kimi')) return 'General';
|
| 40 |
+
if (modelName.includes('GLM')) return 'General';
|
| 41 |
+
return 'Language';
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
const getDisplayName = (modelName: string): string => {
|
| 45 |
+
const parts = modelName.split('/');
|
| 46 |
+
const model = parts[1] || modelName;
|
| 47 |
+
|
| 48 |
+
// Clean up model names for better display
|
| 49 |
+
const cleanName = model
|
| 50 |
+
.replace('-0324', '')
|
| 51 |
+
.replace('-0528', '')
|
| 52 |
+
.replace('-0905', '')
|
| 53 |
+
.replace('-Instruct', '')
|
| 54 |
+
.replace('-A22B', '')
|
| 55 |
+
.replace('-A35B', '')
|
| 56 |
+
.replace('-VL-7B', '')
|
| 57 |
+
.replace('-Exp', '')
|
| 58 |
+
.replace('-Distill-', '-')
|
| 59 |
+
.replace('-Terminus', '')
|
| 60 |
+
.replace('DeepSeek-V3-0324', 'DeepSeek V3')
|
| 61 |
+
.replace('DeepSeek-R1-0528', 'DeepSeek R1')
|
| 62 |
+
.replace('DeepSeek-V3.1', 'DeepSeek V3.1')
|
| 63 |
+
.replace('DeepSeek-V3.1-Terminus', 'DeepSeek V3.1 Terminus')
|
| 64 |
+
.replace('DeepSeek-V3.2-Exp', 'DeepSeek V3.2 Exp')
|
| 65 |
+
.replace('Qwen3-Coder-480B-A35B-Instruct', 'Qwen3 Coder 480B')
|
| 66 |
+
.replace('Qwen2.5-VL-7B-Instruct', 'Qwen2.5 VL 7B')
|
| 67 |
+
.replace('Kimi-K2-Instruct', 'Kimi K2')
|
| 68 |
+
.replace('Kimi-K2-Instruct-0905', 'Kimi K2 0905')
|
| 69 |
+
.replace('Kimi-K2-Thinking', 'Kimi K2 Thinking')
|
| 70 |
+
.replace('GLM-4.6', 'GLM-4.6')
|
| 71 |
+
.replace('MiniMax-M2', 'MiniMax M2')
|
| 72 |
+
.replace('Llama-3.1-8B-Instruct', 'Llama 3.1 8B')
|
| 73 |
+
.replace('Llama-3.1-70B-Instruct', 'Llama 3.1 70B')
|
| 74 |
+
.replace('Llama-3.3-70B-Instruct', 'Llama 3.3 70B')
|
| 75 |
+
.replace('Llama-4-Scout-17B-16E-Instruct', 'Llama 4 Scout 17B')
|
| 76 |
+
.replace('gemma-3-27b-it', 'Gemma 3 27B');
|
| 77 |
+
|
| 78 |
+
return cleanName;
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
export function ModelSelector({ selectedModel, onModelChange, models }: ModelSelectorProps) {
|
| 82 |
+
const [open, setOpen] = useState(false);
|
| 83 |
+
|
| 84 |
+
const getIconForModel = (modelName: string) => {
|
| 85 |
+
for (const [key, icon] of Object.entries(modelIcons)) {
|
| 86 |
+
if (modelName.includes(key)) {
|
| 87 |
+
return icon;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
return <Brain className="w-3 h-3" />;
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
const groupModelsByProvider = () => {
|
| 94 |
+
const groups: Record<string, string[]> = {};
|
| 95 |
+
|
| 96 |
+
models.forEach(model => {
|
| 97 |
+
const provider = getProviderFromModel(model);
|
| 98 |
+
if (!groups[provider]) {
|
| 99 |
+
groups[provider] = [];
|
| 100 |
+
}
|
| 101 |
+
groups[provider].push(model);
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
return groups;
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
const groupedModels = groupModelsByProvider();
|
| 108 |
+
|
| 109 |
+
return (
|
| 110 |
+
<Select
|
| 111 |
+
value={selectedModel}
|
| 112 |
+
onValueChange={onModelChange}
|
| 113 |
+
onOpenChange={setOpen}
|
| 114 |
+
>
|
| 115 |
+
<SelectTrigger className="w-[280px]">
|
| 116 |
+
<div className="flex items-center gap-2 truncate">
|
| 117 |
+
{getIconForModel(selectedModel)}
|
| 118 |
+
<div className="flex-1 min-w-0">
|
| 119 |
+
<div className="flex items-center gap-2">
|
| 120 |
+
<span className="font-medium truncate">
|
| 121 |
+
{getDisplayName(selectedModel)}
|
| 122 |
+
</span>
|
| 123 |
+
<Badge variant="outline" className="text-xs">
|
| 124 |
+
{getModelCategory(selectedModel)}
|
| 125 |
+
</Badge>
|
| 126 |
+
</div>
|
| 127 |
+
<div className="text-xs text-muted-foreground truncate">
|
| 128 |
+
{getProviderFromModel(selectedModel)}
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</SelectTrigger>
|
| 133 |
+
|
| 134 |
+
<SelectContent>
|
| 135 |
+
{Object.entries(groupedModels).map(([provider, providerModels]) => (
|
| 136 |
+
<div key={provider}>
|
| 137 |
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground border-b">
|
| 138 |
+
{provider}
|
| 139 |
+
</div>
|
| 140 |
+
{providerModels.map((model) => (
|
| 141 |
+
<SelectItem key={model} value={model}>
|
| 142 |
+
<div className="flex items-center gap-2 w-full">
|
| 143 |
+
{getIconForModel(model)}
|
| 144 |
+
<div className="flex-1 min-w-0">
|
| 145 |
+
<div className="flex items-center gap-2">
|
| 146 |
+
<span className="font-medium truncate">
|
| 147 |
+
{getDisplayName(model)}
|
| 148 |
+
</span>
|
| 149 |
+
<Badge variant="outline" className="text-xs flex-shrink-0">
|
| 150 |
+
{getModelCategory(model)}
|
| 151 |
+
</Badge>
|
| 152 |
+
</div>
|
| 153 |
+
<div className="text-xs text-muted-foreground truncate">
|
| 154 |
+
{model}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
</SelectItem>
|
| 159 |
+
))}
|
| 160 |
+
</div>
|
| 161 |
+
))}
|
| 162 |
+
</SelectContent>
|
| 163 |
+
</Select>
|
| 164 |
+
);
|
| 165 |
+
}
|
providers.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { ReactNode } from 'react';
|
| 4 |
+
|
| 5 |
+
interface ProvidersProps {
|
| 6 |
+
children: ReactNode;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export function Providers({ children }: ProvidersProps) {
|
| 10 |
+
return (
|
| 11 |
+
<div>
|
| 12 |
+
{children}
|
| 13 |
+
</div>
|
| 14 |
+
);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Temporary simple provider until we fix AI SDK dependencies
|
| 18 |
+
export const supportedModels = {};
|
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 }
|
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 }
|
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 |
+
HTMLDivElement,
|
| 34 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 35 |
+
>(({ className, ...props }, ref) => (
|
| 36 |
+
<div
|
| 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 |
+
HTMLDivElement,
|
| 49 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 50 |
+
>(({ className, ...props }, ref) => (
|
| 51 |
+
<div
|
| 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 }
|
ui/select.tsx
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
| 3 |
+
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Select = SelectPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const SelectGroup = SelectPrimitive.Group
|
| 10 |
+
|
| 11 |
+
const SelectValue = SelectPrimitive.Value
|
| 12 |
+
|
| 13 |
+
const SelectTrigger = React.forwardRef<
|
| 14 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
| 15 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
| 16 |
+
>(({ className, children, ...props }, ref) => (
|
| 17 |
+
<SelectPrimitive.Trigger
|
| 18 |
+
ref={ref}
|
| 19 |
+
className={cn(
|
| 20 |
+
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
| 21 |
+
className
|
| 22 |
+
)}
|
| 23 |
+
{...props}
|
| 24 |
+
>
|
| 25 |
+
{children}
|
| 26 |
+
<SelectPrimitive.Icon asChild>
|
| 27 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
| 28 |
+
</SelectPrimitive.Icon>
|
| 29 |
+
</SelectPrimitive.Trigger>
|
| 30 |
+
))
|
| 31 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
| 32 |
+
|
| 33 |
+
const SelectScrollUpButton = React.forwardRef<
|
| 34 |
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
| 35 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
| 36 |
+
>(({ className, ...props }, ref) => (
|
| 37 |
+
<SelectPrimitive.ScrollUpButton
|
| 38 |
+
ref={ref}
|
| 39 |
+
className={cn(
|
| 40 |
+
"flex cursor-default items-center justify-center py-1",
|
| 41 |
+
className
|
| 42 |
+
)}
|
| 43 |
+
{...props}
|
| 44 |
+
>
|
| 45 |
+
<ChevronUp className="h-4 w-4" />
|
| 46 |
+
</SelectPrimitive.ScrollUpButton>
|
| 47 |
+
))
|
| 48 |
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
| 49 |
+
|
| 50 |
+
const SelectScrollDownButton = React.forwardRef<
|
| 51 |
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
| 52 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
| 53 |
+
>(({ className, ...props }, ref) => (
|
| 54 |
+
<SelectPrimitive.ScrollDownButton
|
| 55 |
+
ref={ref}
|
| 56 |
+
className={cn(
|
| 57 |
+
"flex cursor-default items-center justify-center py-1",
|
| 58 |
+
className
|
| 59 |
+
)}
|
| 60 |
+
{...props}
|
| 61 |
+
>
|
| 62 |
+
<ChevronDown className="h-4 w-4" />
|
| 63 |
+
</SelectPrimitive.ScrollDownButton>
|
| 64 |
+
))
|
| 65 |
+
SelectScrollDownButton.displayName =
|
| 66 |
+
SelectPrimitive.ScrollDownButton.displayName
|
| 67 |
+
|
| 68 |
+
const SelectContent = React.forwardRef<
|
| 69 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
| 70 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
| 71 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
| 72 |
+
<SelectPrimitive.Portal>
|
| 73 |
+
<SelectPrimitive.Content
|
| 74 |
+
ref={ref}
|
| 75 |
+
className={cn(
|
| 76 |
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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",
|
| 77 |
+
position === "popper" &&
|
| 78 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
| 79 |
+
className
|
| 80 |
+
)}
|
| 81 |
+
position={position}
|
| 82 |
+
{...props}
|
| 83 |
+
>
|
| 84 |
+
<SelectScrollUpButton />
|
| 85 |
+
<SelectPrimitive.Viewport
|
| 86 |
+
className={cn(
|
| 87 |
+
"p-1",
|
| 88 |
+
position === "popper" &&
|
| 89 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
| 90 |
+
)}
|
| 91 |
+
>
|
| 92 |
+
{children}
|
| 93 |
+
</SelectPrimitive.Viewport>
|
| 94 |
+
<SelectScrollDownButton />
|
| 95 |
+
</SelectPrimitive.Content>
|
| 96 |
+
</SelectPrimitive.Portal>
|
| 97 |
+
))
|
| 98 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
| 99 |
+
|
| 100 |
+
const SelectLabel = React.forwardRef<
|
| 101 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
| 102 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
| 103 |
+
>(({ className, ...props }, ref) => (
|
| 104 |
+
<SelectPrimitive.Label
|
| 105 |
+
ref={ref}
|
| 106 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
| 107 |
+
{...props}
|
| 108 |
+
/>
|
| 109 |
+
))
|
| 110 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
| 111 |
+
|
| 112 |
+
const SelectItem = React.forwardRef<
|
| 113 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
| 114 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
| 115 |
+
>(({ className, children, ...props }, ref) => (
|
| 116 |
+
<SelectPrimitive.Item
|
| 117 |
+
ref={ref}
|
| 118 |
+
className={cn(
|
| 119 |
+
"relative flex w-full 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",
|
| 120 |
+
className
|
| 121 |
+
)}
|
| 122 |
+
{...props}
|
| 123 |
+
>
|
| 124 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 125 |
+
<SelectPrimitive.ItemIndicator>
|
| 126 |
+
<Check className="h-4 w-4" />
|
| 127 |
+
</SelectPrimitive.ItemIndicator>
|
| 128 |
+
</span>
|
| 129 |
+
|
| 130 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 131 |
+
</SelectPrimitive.Item>
|
| 132 |
+
))
|
| 133 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
| 134 |
+
|
| 135 |
+
const SelectSeparator = React.forwardRef<
|
| 136 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
| 137 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
| 138 |
+
>(({ className, ...props }, ref) => (
|
| 139 |
+
<SelectPrimitive.Separator
|
| 140 |
+
ref={ref}
|
| 141 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 142 |
+
{...props}
|
| 143 |
+
/>
|
| 144 |
+
))
|
| 145 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
| 146 |
+
|
| 147 |
+
export {
|
| 148 |
+
Select,
|
| 149 |
+
SelectGroup,
|
| 150 |
+
SelectValue,
|
| 151 |
+
SelectTrigger,
|
| 152 |
+
SelectContent,
|
| 153 |
+
SelectLabel,
|
| 154 |
+
SelectItem,
|
| 155 |
+
SelectSeparator,
|
| 156 |
+
SelectScrollUpButton,
|
| 157 |
+
SelectScrollDownButton,
|
| 158 |
+
}
|
ui/tabs.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Tabs = TabsPrimitive.Root
|
| 7 |
+
|
| 8 |
+
const TabsList = React.forwardRef<
|
| 9 |
+
React.ElementRef<typeof TabsPrimitive.List>,
|
| 10 |
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
| 11 |
+
>(({ className, ...props }, ref) => (
|
| 12 |
+
<TabsPrimitive.List
|
| 13 |
+
ref={ref}
|
| 14 |
+
className={cn(
|
| 15 |
+
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
| 16 |
+
className
|
| 17 |
+
)}
|
| 18 |
+
{...props}
|
| 19 |
+
/>
|
| 20 |
+
))
|
| 21 |
+
TabsList.displayName = TabsPrimitive.List.displayName
|
| 22 |
+
|
| 23 |
+
const TabsTrigger = React.forwardRef<
|
| 24 |
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
| 25 |
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
| 26 |
+
>(({ className, ...props }, ref) => (
|
| 27 |
+
<TabsPrimitive.Trigger
|
| 28 |
+
ref={ref}
|
| 29 |
+
className={cn(
|
| 30 |
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
| 31 |
+
className
|
| 32 |
+
)}
|
| 33 |
+
{...props}
|
| 34 |
+
/>
|
| 35 |
+
))
|
| 36 |
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
| 37 |
+
|
| 38 |
+
const TabsContent = React.forwardRef<
|
| 39 |
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
| 40 |
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
| 41 |
+
>(({ className, ...props }, ref) => (
|
| 42 |
+
<TabsPrimitive.Content
|
| 43 |
+
ref={ref}
|
| 44 |
+
className={cn(
|
| 45 |
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
| 46 |
+
className
|
| 47 |
+
)}
|
| 48 |
+
{...props}
|
| 49 |
+
/>
|
| 50 |
+
))
|
| 51 |
+
TabsContent.displayName = TabsPrimitive.Content.displayName
|
| 52 |
+
|
| 53 |
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
ui/textarea.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
export interface TextareaProps
|
| 6 |
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
| 7 |
+
|
| 8 |
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
| 9 |
+
({ className, ...props }, ref) => {
|
| 10 |
+
return (
|
| 11 |
+
<textarea
|
| 12 |
+
className={cn(
|
| 13 |
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
| 14 |
+
className
|
| 15 |
+
)}
|
| 16 |
+
ref={ref}
|
| 17 |
+
{...props}
|
| 18 |
+
/>
|
| 19 |
+
)
|
| 20 |
+
}
|
| 21 |
+
)
|
| 22 |
+
Textarea.displayName = "Textarea"
|
| 23 |
+
|
| 24 |
+
export { Textarea }
|