files cant't be seen by LLM, but everything else works.

This commit is contained in:
emil
2026-04-26 22:52:49 +03:00
parent 55d4b7dbba
commit 2307c63bf1
16 changed files with 861 additions and 134 deletions
+2 -1
View File
@@ -105,7 +105,8 @@ export default function ChatArea({ conversationId }: ChatAreaProps) {
settings.temperature,
settings.maxTokens,
systemPrompt,
abortControllerRef.current.signal
abortControllerRef.current.signal,
conversationId
);
for await (const chunk of stream) {
+100 -59
View File
@@ -1,10 +1,12 @@
import { useState, useEffect } from 'react';
import { X, RefreshCw, Eye, EyeOff } from 'lucide-react';
import { X, RefreshCw, Eye, EyeOff, BarChart3 } from 'lucide-react';
import { useChatStore } from '@/stores/chatStore';
import { fetchModels } from '@/services/api';
import TokenUsagePanel from './TokenUsagePanel';
export default function SettingsModal() {
const { settings, updateSettings, toggleSettings, closeSettings } = useChatStore();
const [activeTab, setActiveTab] = useState<'settings' | 'tokens'>('settings');
const [localSettings, setLocalSettings] = useState(settings);
const [models, setModels] = useState<string[]>([]);
const [isLoadingModels, setIsLoadingModels] = useState(false);
@@ -52,20 +54,51 @@ export default function SettingsModal() {
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={toggleSettings}
/>
<div className="relative bg-[var(--color-surface)] border border-[var(--color-border)] rounded-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto shadow-2xl">
<div className="relative bg-[var(--color-surface)] border border-[var(--color-border)] rounded-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto shadow-2xl flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border)]">
<h2 className="text-xl font-semibold text-[var(--color-text)]">Settings</h2>
<button
onClick={toggleSettings}
className="p-2 rounded-lg hover:bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
>
<X size={20} />
</button>
<div className="flex-shrink-0">
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border)]">
<h2 className="text-xl font-semibold text-[var(--color-text)]">Settings</h2>
<button
onClick={toggleSettings}
className="p-2 rounded-lg hover:bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
>
<X size={20} />
</button>
</div>
{/* Tabs */}
<div className="flex gap-1 px-6 pb-2">
<button
onClick={() => setActiveTab('settings')}
className={`flex-1 px-4 py-2 rounded-t-lg text-sm font-medium transition-colors ${
activeTab === 'settings'
? 'bg-[var(--color-bg)] text-[var(--color-text)] border-t-2 border-[var(--color-accent)]'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
}`}
>
Configuration
</button>
<button
onClick={() => setActiveTab('tokens')}
className={`flex-1 px-4 py-2 rounded-t-lg text-sm font-medium transition-colors ${
activeTab === 'tokens'
? 'bg-[var(--color-bg)] text-[var(--color-text)] border-t-2 border-[var(--color-accent)]'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
}`}
>
<div className="flex items-center justify-center gap-1.5">
<BarChart3 size={14} />
<span>Token Usage</span>
</div>
</button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-5">
<div className="flex-1 overflow-y-auto">
{activeTab === 'settings' ? (
<div className="p-6 space-y-5">
{error && (
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
{error}
@@ -217,59 +250,67 @@ export default function SettingsModal() {
/>
</div>
<hr className="border-[var(--color-border)]" />
<hr className="border-[var(--color-border)]" />
{/* Embedding Model */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
Embedding Model
</label>
<input
type="text"
value={localSettings.embeddingModel}
onChange={(e) =>
setLocalSettings((prev) => ({ ...prev, embeddingModel: e.target.value }))
}
placeholder="text-embedding-3-small"
className="input-field"
/>
<p className="text-xs text-[var(--color-text-muted)]">
Model used for knowledge base embeddings
</p>
</div>
{/* Embedding Model */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
Embedding Model
</label>
<input
type="text"
value={localSettings.embeddingModel}
onChange={(e) =>
setLocalSettings((prev) => ({ ...prev, embeddingModel: e.target.value }))
}
placeholder="text-embedding-3-small"
className="input-field"
/>
<p className="text-xs text-[var(--color-text-muted)]">
Model used for knowledge base embeddings
</p>
</div>
{/* ChromaDB Directory */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
ChromaDB Directory
</label>
<input
type="text"
value={localSettings.chromaDir}
onChange={(e) =>
setLocalSettings((prev) => ({ ...prev, chromaDir: e.target.value }))
}
placeholder="./data/chromadb"
className="input-field"
/>
<p className="text-xs text-[var(--color-text-muted)]">
Path to ChromaDB persistent storage on the server
</p>
</div>
{/* ChromaDB Directory */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
ChromaDB Directory
</label>
<input
type="text"
value={localSettings.chromaDir}
onChange={(e) =>
setLocalSettings((prev) => ({ ...prev, chromaDir: e.target.value }))
}
placeholder="./data/chromadb"
className="input-field"
/>
<p className="text-xs text-[var(--color-text-muted)]">
Path to ChromaDB persistent storage on the server
</p>
</div>
</div>
) : (
<div className="p-6">
<TokenUsagePanel />
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--color-border)]">
<button
onClick={toggleSettings}
className="px-4 py-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-surface-hover)] transition-colors"
>
Cancel
</button>
<button onClick={handleSave} className="btn-primary">
Save Settings
</button>
</div>
{activeTab === 'settings' && (
<div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--color-border)] shrink-0">
<button
onClick={toggleSettings}
className="px-4 py-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-surface-hover)] transition-colors"
>
Cancel
</button>
<button onClick={handleSave} className="btn-primary">
Save Settings
</button>
</div>
)}
</div>
</div>
);
@@ -0,0 +1,282 @@
import { useState, useEffect } from 'react';
import { RefreshCw, Trash2, BarChart3 } from 'lucide-react';
interface TokenUsageSummary {
promptTokens: number;
completionTokens: number;
totalTokens: number;
requestCount: number;
}
interface ModelBreakdown {
[key: string]: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
requestCount: number;
};
}
interface TokenUsageHistory {
timestamp: number;
conversationId: string;
model: string;
promptTokens: number;
completionTokens: number;
totalTokens: number;
}
interface TokenUsageData {
summary: TokenUsageSummary;
modelBreakdown: ModelBreakdown;
history: TokenUsageHistory[];
}
type TimePeriod = 'all' | 'today' | 'week';
export default function TokenUsagePanel() {
const [data, setData] = useState<TokenUsageData | null>(null);
const [loading, setLoading] = useState(false);
const [period, setPeriod] = useState<TimePeriod>('all');
const [expandedModel, setExpandedModel] = useState<string | null>(null);
const fetchData = async () => {
setLoading(true);
try {
const res = await fetch(`/api/token-usage?period=${period}`);
const result = await res.json();
setData(result);
} catch (err) {
console.error('Failed to fetch token usage:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [period]);
const handleClear = async () => {
if (!confirm('Clear all token usage data?')) return;
try {
await fetch('/api/token-usage', { method: 'DELETE' });
setData(null);
fetchData();
} catch (err) {
console.error('Failed to clear token usage:', err);
}
};
const formatNumber = (n: number) => n.toLocaleString();
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
};
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-[var(--color-text)]">
<BarChart3 size={20} className="text-[var(--color-accent)]" />
<h3 className="text-sm font-medium">Token Usage</h3>
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchData}
disabled={loading}
className="p-2 rounded-lg hover:bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors disabled:opacity-50"
>
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
</button>
<button
onClick={handleClear}
disabled={!data || data.summary.requestCount === 0}
className="p-2 rounded-lg hover:bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)] hover:text-red-400 transition-colors disabled:opacity-50 disabled:pointer-events-none"
>
<Trash2 size={16} />
</button>
</div>
</div>
{/* Period filter */}
<div className="flex gap-1 p-1 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
{(['all', 'today', 'week'] as TimePeriod[]).map((p) => {
const labels = { all: 'All', today: 'Today', week: 'Week' };
return (
<button
key={p}
onClick={() => setPeriod(p)}
className={`flex-1 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
period === p
? 'bg-[var(--color-accent)] text-white'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
}`}
>
{labels[p]}
</button>
);
})}
</div>
{/* Summary */}
{data && data.summary.requestCount > 0 ? (
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
<p className="text-xs text-[var(--color-text-muted)] mb-1">Total Tokens</p>
<p className="text-lg font-semibold text-[var(--color-text)]">
{formatNumber(data.summary.totalTokens)}
</p>
</div>
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
<p className="text-xs text-[var(--color-text-muted)] mb-1">Requests</p>
<p className="text-lg font-semibold text-[var(--color-text)]">
{data.summary.requestCount}
</p>
</div>
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
<p className="text-xs text-[var(--color-text-muted)] mb-1">Prompt Tokens</p>
<p className="text-sm font-medium text-[var(--color-text)]">
{formatNumber(data.summary.promptTokens)}
</p>
</div>
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
<p className="text-xs text-[var(--color-text-muted)] mb-1">Completion Tokens</p>
<p className="text-sm font-medium text-[var(--color-text)]">
{formatNumber(data.summary.completionTokens)}
</p>
</div>
</div>
) : (
<div className="text-center py-8 text-[var(--color-text-muted)]">
{loading ? 'Loading...' : 'No token usage data yet'}
</div>
)}
{/* Model breakdown */}
{data && Object.keys(data.modelBreakdown).length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-[var(--color-text)]">By Model</p>
{Object.entries(data.modelBreakdown)
.sort((a, b) => b[1].totalTokens - a[1].totalTokens)
.map(([model, stats]) => (
<div
key={model}
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] overflow-hidden"
>
<button
onClick={() => setExpandedModel(expandedModel === model ? null : model)}
className="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-[var(--color-surface-hover)] transition-colors"
>
<div className="min-w-0">
<p className="text-sm font-medium text-[var(--color-text)] truncate">
{model}
</p>
<p className="text-xs text-[var(--color-text-muted)]">
{formatNumber(stats.totalTokens)} tokens · {stats.requestCount} requests
</p>
</div>
<div className="text-right shrink-0 ml-4">
<p className="text-xs text-[var(--color-text-secondary)]">
{formatNumber(stats.promptTokens)} / {formatNumber(stats.completionTokens)}
</p>
</div>
</button>
{expandedModel === model && (
<div className="px-4 pb-3 border-t border-[var(--color-border)] pt-3">
{/* Simple bar chart */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-xs text-[var(--color-text-muted)] w-16">Prompt</span>
<div className="flex-1 h-3 rounded bg-[var(--color-surface)] overflow-hidden">
<div
className="h-full rounded bg-[var(--color-accent)] transition-all"
style={{
width: `${
stats.totalTokens > 0
? (stats.promptTokens / stats.totalTokens) * 100
: 0
}%`,
}}
/>
</div>
<span className="text-xs text-[var(--color-text)] w-16 text-right">
{formatNumber(stats.promptTokens)}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[var(--color-text-muted)] w-16">Completion</span>
<div className="flex-1 h-3 rounded bg-[var(--color-surface)] overflow-hidden">
<div
className="h-full rounded bg-green-500 transition-all"
style={{
width: `${
stats.totalTokens > 0
? (stats.completionTokens / stats.totalTokens) * 100
: 0
}%`,
}}
/>
</div>
<span className="text-xs text-[var(--color-text)] w-16 text-right">
{formatNumber(stats.completionTokens)}
</span>
</div>
</div>
{/* Show recent history for this model */}
{data.history.filter((h) => h.model === model).length > 0 && (
<div className="mt-3 pt-3 border-t border-[var(--color-border)]">
<p className="text-xs text-[var(--color-text-muted)] mb-2">Recent requests</p>
<div className="space-y-1 max-h-32 overflow-y-auto">
{data.history
.filter((h) => h.model === model)
.slice(-5)
.reverse()
.map((entry, i) => (
<div
key={i}
className="flex items-center justify-between text-xs px-2 py-1 rounded bg-[var(--color-surface)]"
>
<span className="text-[var(--color-text-muted)]">{formatTime(entry.timestamp)}</span>
<span className="text-[var(--color-text)]">
{formatNumber(entry.totalTokens)} tokens
</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
))}
</div>
)}
{/* History timeline */}
{data && data.history.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-[var(--color-text)]">Recent History</p>
{data.history.slice(-10).reverse().map((entry, i) => (
<div
key={i}
className="flex items-center gap-3 text-xs px-3 py-2 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]"
>
<span className="text-[var(--color-text-muted)] w-12 shrink-0">
{formatTime(entry.timestamp)}
</span>
<span className="text-[var(--color-text)] truncate flex-1">{entry.model}</span>
<span className="text-[var(--color-text-secondary)] shrink-0">
{formatNumber(entry.totalTokens)}
</span>
</div>
))}
</div>
)}
</div>
);
}
@@ -1,7 +1,15 @@
import { useState, useEffect } from 'react';
import { X, Plus, Trash2, Upload, BookOpen, FileText, Database, ChevronDown, ChevronUp } from 'lucide-react';
import { useState, useEffect, useRef } from 'react';
import { X, Plus, Trash2, Upload, BookOpen, FileText, Database, ChevronDown, ChevronUp, FolderOpen, AlertCircle } from 'lucide-react';
import { useChatStore } from '@/stores/chatStore';
interface FileResult {
id: string;
originalName: string;
size: number;
chunkCount?: number;
}
export default function KnowledgePanel() {
const { settings, toggleKnowledge, isKnowledgeOpen, setKnowledgeBases, knowledgeBases, removeKnowledgeBase } = useChatStore();
const [newName, setNewName] = useState('');
@@ -10,6 +18,10 @@ export default function KnowledgePanel() {
const [expandedBase, setExpandedBase] = useState<string | null>(null);
const [uploadingBase, setUploadingBase] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [folderLoading, setFolderLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const folderInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isKnowledgeOpen) loadBases();
@@ -17,11 +29,15 @@ export default function KnowledgePanel() {
const loadBases = async () => {
try {
setError(null);
const res = await fetch('/api/knowledge');
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
setKnowledgeBases(data.bases || []);
} catch (err) {
console.error('Failed to load knowledge bases:', err);
setError('Could not connect to knowledge base service. Is the backend running?');
setKnowledgeBases([]);
}
};
@@ -35,12 +51,13 @@ export default function KnowledgePanel() {
body: JSON.stringify({ name: newName.trim(), description: newDesc.trim() }),
});
const base = await res.json();
if (base.error) throw new Error(base.error);
setKnowledgeBases([...knowledgeBases, base]);
setNewName('');
setNewDesc('');
setIsCreating(false);
} catch (err) {
alert('Failed to create knowledge base');
} catch (err: any) {
setError(err.message || 'Failed to create knowledge base');
} finally {
setLoading(false);
}
@@ -52,7 +69,7 @@ export default function KnowledgePanel() {
await fetch(`/api/knowledge/${id}`, { method: 'DELETE' });
removeKnowledgeBase(id);
} catch (err) {
alert('Failed to delete knowledge base');
setError('Failed to delete knowledge base');
}
};
@@ -60,6 +77,7 @@ export default function KnowledgePanel() {
if (!e.target.files?.[0]) return;
const file = e.target.files[0];
setUploadingBase(baseId);
setError(null);
try {
const formData = new FormData();
@@ -76,7 +94,6 @@ export default function KnowledgePanel() {
if (!res.ok) throw new Error('Upload failed');
const data = await res.json();
// Update local state
const updated = knowledgeBases.map((b) =>
b.id === baseId
? { ...b, files: [...b.files, data.file], documentCount: (b.documentCount || 0) + data.chunks }
@@ -84,13 +101,68 @@ export default function KnowledgePanel() {
);
setKnowledgeBases(updated);
} catch (err) {
alert('Failed to upload file. Make sure API settings are configured.');
setError('Failed to upload file. Make sure API settings are configured.');
} finally {
setUploadingBase(null);
e.target.value = '';
}
};
const handleUploadFolder = async (baseId: string, e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
setFolderLoading(true);
setError(null);
// For browser folder upload, we can't get the actual folder path.
// Instead, upload all files from the file picker using webkitdirectory.
const formData = new FormData();
Array.from(files).forEach((f) => formData.append('files', f));
formData.append('apiUrl', settings.apiUrl);
formData.append('apiKey', settings.apiKey);
formData.append('embeddingModel', settings.embeddingModel);
try {
// Use a batch upload approach — upload files one by one
const uploaded: FileResult[] = [];
const errors: string[] = [];
for (const file of Array.from(files)) {
const singleForm = new FormData();
singleForm.append('file', file);
singleForm.append('apiUrl', settings.apiUrl);
singleForm.append('apiKey', settings.apiKey);
singleForm.append('embeddingModel', settings.embeddingModel);
try {
const res = await fetch(`/api/knowledge/${baseId}/files`, {
method: 'POST',
body: singleForm,
});
if (!res.ok) throw new Error('Failed');
const data = await res.json();
uploaded.push(data.file);
} catch {
errors.push(file.name);
}
}
if (uploaded.length > 0) {
await loadBases();
}
if (errors.length > 0) {
setError(`${errors.length} files failed to upload out of ${files.length}`);
}
} catch (err) {
setError('Folder upload failed');
} finally {
setFolderLoading(false);
e.target.value = '';
}
};
const handleDeleteFile = async (baseId: string, fileId: string) => {
try {
await fetch(`/api/knowledge/${baseId}/files/${fileId}`, { method: 'DELETE' });
@@ -105,7 +177,7 @@ export default function KnowledgePanel() {
);
setKnowledgeBases(updated);
} catch (err) {
alert('Failed to delete file');
setError('Failed to delete file');
}
};
@@ -131,6 +203,17 @@ export default function KnowledgePanel() {
{/* Content */}
<div className="flex-1 overflow-y-auto p-5 space-y-4">
{/* Error */}
{error && (
<div className="flex items-center gap-2 px-4 py-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
<AlertCircle size={16} />
{error}
<button onClick={() => setError(null)} className="ml-auto text-red-400 hover:text-red-300">
<X size={14} />
</button>
</div>
)}
{/* Create new */}
{!isCreating ? (
<button
@@ -203,7 +286,7 @@ export default function KnowledgePanel() {
{base.name}
</p>
<p className="text-xs text-[var(--color-text-muted)]">
{base.documentCount || 0} chunks · {base.files.length} files
{base.documentCount || 0} chunks · {base.files?.length || 0} files
</p>
</div>
</button>
@@ -222,7 +305,7 @@ export default function KnowledgePanel() {
)}
{/* Files */}
{base.files.length > 0 && (
{base.files && base.files.length > 0 && (
<div className="space-y-1.5">
{base.files.map((file) => (
<div
@@ -235,7 +318,7 @@ export default function KnowledgePanel() {
{file.originalName}
</span>
<span className="text-[var(--color-text-muted)] shrink-0">
({file.chunkCount} chunks)
({file.chunkCount || 0} chunks)
</span>
</div>
<button
@@ -249,25 +332,47 @@ export default function KnowledgePanel() {
</div>
)}
{/* Upload */}
<div className="relative">
<label className={`flex items-center justify-center gap-2 w-full px-3 py-2 rounded-lg border border-dashed border-[var(--color-border)] text-xs text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:text-[var(--color-accent)] transition-colors cursor-pointer ${uploadingBase === base.id ? 'opacity-50' : ''}`}>
{/* Upload buttons */}
<div className="flex gap-2">
<label className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border border-dashed border-[var(--color-border)] text-xs text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:text-[var(--color-accent)] transition-colors cursor-pointer ${uploadingBase === base.id ? 'opacity-50' : ''}`}>
{uploadingBase === base.id ? (
<span className="animate-pulse">Processing...</span>
) : (
<>
<Upload size={14} />
<span>Upload document</span>
<span>Upload file</span>
</>
)}
<input
type="file"
ref={fileInputRef}
accept=".txt,.md,.pdf,.docx,.json,.csv,.js,.ts,.html,.css"
onChange={(e) => handleUploadFile(base.id, e)}
disabled={uploadingBase === base.id || folderLoading}
className="hidden"
/>
</label>
<label className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border border-dashed border-[var(--color-border)] text-xs text-[var(--color-text-muted)] hover:border-green-500 hover:text-green-500 transition-colors cursor-pointer ${folderLoading ? 'opacity-50' : ''}`}>
{folderLoading ? (
<span className="animate-pulse">Scanning folder...</span>
) : (
<>
<FolderOpen size={14} />
<span>Upload folder</span>
</>
)}
<input
type="file"
ref={folderInputRef}
// @ts-ignore — webkitdirectory is non-standard but widely supported
webkitdirectory=""
directory=""
multiple
onChange={(e) => handleUploadFolder(base.id, e)}
disabled={uploadingBase === base.id || folderLoading}
className="hidden"
/>
</label>
<input
type="file"
accept=".txt,.md,.pdf,.docx"
onChange={(e) => handleUploadFile(base.id, e)}
disabled={uploadingBase === base.id}
className="hidden"
/>
</div>
</div>
)}
+3 -1
View File
@@ -8,7 +8,8 @@ export async function* streamChatCompletion(
temperature: number,
maxTokens: number,
systemPrompt: string,
abortSignal: AbortSignal
abortSignal: AbortSignal,
conversationId?: string
): AsyncGenerator<OpenAIStreamChunk, void, unknown> {
const formattedMessages: OpenAIMessage[] = [
{ role: 'system', content: systemPrompt },
@@ -28,6 +29,7 @@ export async function* streamChatCompletion(
max_tokens: maxTokens,
messages: formattedMessages,
stream: true,
conversationId,
}),
signal: abortSignal,
});