|
|
@@ -0,0 +1,659 @@
|
|
|
+// Dedicated workspace edit page with system prompt editor
|
|
|
+
|
|
|
+import { useState, useEffect, useCallback } from 'react'
|
|
|
+import { useParams, useNavigate } from 'react-router-dom'
|
|
|
+import Editor from '@monaco-editor/react'
|
|
|
+import apiClient from '@/api/client'
|
|
|
+import { useWorkspace } from '@/contexts/WorkspaceContext'
|
|
|
+import { useAuth } from '@/contexts/AuthContext'
|
|
|
+import Button from '@/components/Button'
|
|
|
+import Input from '@/components/Input'
|
|
|
+import ModelSelector from '@/components/ModelSelector'
|
|
|
+import type { Workspace } from '@/types'
|
|
|
+import type { LlmProvider, LlmModel } from '@/types/chat'
|
|
|
+import * as chatApi from '@/api/chat'
|
|
|
+
|
|
|
+interface WorkspaceSettings {
|
|
|
+ llm_provider_id?: string
|
|
|
+ llm_model_id?: string
|
|
|
+ llm_helper_provider_id?: string
|
|
|
+ llm_helper_model_id?: string
|
|
|
+ llm_context_compaction_threshold?: number
|
|
|
+ llm_system_prompt?: string
|
|
|
+ llm_helper_system_prompt?: string
|
|
|
+ [key: string]: unknown
|
|
|
+}
|
|
|
+
|
|
|
+// Default system prompt template with access to storage and user
|
|
|
+const DEFAULT_SYSTEM_PROMPT = `// System prompt generator function
|
|
|
+// This function is called before each LLM request
|
|
|
+// Available context:
|
|
|
+// - user: { id, name, email }
|
|
|
+// - workspace: { id, name, settings }
|
|
|
+// - storage: async function to query collections
|
|
|
+//
|
|
|
+// Example storage query:
|
|
|
+// const customers = await storage.query('customers', { limit: 10 })
|
|
|
+//
|
|
|
+// Return a string with the system prompt
|
|
|
+
|
|
|
+async function generateSystemPrompt(context) {
|
|
|
+ const { user, workspace } = context;
|
|
|
+
|
|
|
+ return \`You are a helpful AI assistant for \${workspace.name}.
|
|
|
+You are helping \${user.name} (\${user.email}).
|
|
|
+
|
|
|
+Be concise and helpful. Focus on the user's needs.
|
|
|
+\`;
|
|
|
+}
|
|
|
+`
|
|
|
+
|
|
|
+// Default helper system prompt for title generation and compression
|
|
|
+const DEFAULT_HELPER_SYSTEM_PROMPT = `// Helper model system prompt generator
|
|
|
+// Used for: conversation title generation, context compression
|
|
|
+// This should be optimized for fast, concise responses
|
|
|
+//
|
|
|
+// Available context:
|
|
|
+// - user: { id, name, email }
|
|
|
+// - workspace: { id, name, settings }
|
|
|
+// - task: 'title_generation' | 'compression'
|
|
|
+
|
|
|
+async function generateSystemPrompt(context) {
|
|
|
+ const { task } = context;
|
|
|
+
|
|
|
+ if (task === 'title_generation') {
|
|
|
+ return \`Generate a short, descriptive title (max 6 words) for the conversation.
|
|
|
+Return ONLY the title, no quotes or explanation.\`;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (task === 'compression') {
|
|
|
+ return \`Summarize the conversation concisely while preserving key information.
|
|
|
+Focus on: decisions made, questions asked, important context.
|
|
|
+Be brief but complete.\`;
|
|
|
+ }
|
|
|
+
|
|
|
+ return 'You are a helpful assistant.';
|
|
|
+}
|
|
|
+`
|
|
|
+
|
|
|
+function WorkspaceEdit() {
|
|
|
+ const { workspaceId } = useParams<{ workspaceId: string }>()
|
|
|
+ const navigate = useNavigate()
|
|
|
+ const { fetchWorkspaces: refreshWorkspaces } = useWorkspace()
|
|
|
+ const { user } = useAuth()
|
|
|
+
|
|
|
+ const [workspace, setWorkspace] = useState<Workspace | null>(null)
|
|
|
+ const [isLoading, setIsLoading] = useState(true)
|
|
|
+ const [isSaving, setIsSaving] = useState(false)
|
|
|
+ const [error, setError] = useState<string | null>(null)
|
|
|
+ const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
|
|
+
|
|
|
+ // Form state
|
|
|
+ const [name, setName] = useState('')
|
|
|
+ const [systemPrompt, setSystemPrompt] = useState(DEFAULT_SYSTEM_PROMPT)
|
|
|
+ const [helperSystemPrompt, setHelperSystemPrompt] = useState(DEFAULT_HELPER_SYSTEM_PROMPT)
|
|
|
+
|
|
|
+ // LLM Provider and Model state
|
|
|
+ const [providers, setProviders] = useState<LlmProvider[]>([])
|
|
|
+ const [models, setModels] = useState<LlmModel[]>([])
|
|
|
+ const [isLoadingProviders, setIsLoadingProviders] = useState(true)
|
|
|
+ const [isLoadingModels, setIsLoadingModels] = useState(false)
|
|
|
+ const [selectedProviderId, setSelectedProviderId] = useState<string>('')
|
|
|
+ const [selectedModelId, setSelectedModelId] = useState<string>('')
|
|
|
+
|
|
|
+ // Helper model state
|
|
|
+ const [helperProviderId, setHelperProviderId] = useState<string>('')
|
|
|
+ const [helperModelId, setHelperModelId] = useState<string>('')
|
|
|
+ const [helperModels, setHelperModels] = useState<LlmModel[]>([])
|
|
|
+ const [isLoadingHelperModels, setIsLoadingHelperModels] = useState(false)
|
|
|
+ const [compactionThreshold, setCompactionThreshold] = useState<number>(80)
|
|
|
+
|
|
|
+ // Track if initial load is done to prevent clearing models
|
|
|
+ const [initialLoadDone, setInitialLoadDone] = useState(false)
|
|
|
+
|
|
|
+ // Test prompt result modal
|
|
|
+ const [testPromptResult, setTestPromptResult] = useState<{ success: boolean; content: string } | null>(null)
|
|
|
+
|
|
|
+ // Load workspace data
|
|
|
+ const loadWorkspace = useCallback(async () => {
|
|
|
+ if (!workspaceId) return
|
|
|
+
|
|
|
+ setIsLoading(true)
|
|
|
+ setError(null)
|
|
|
+ try {
|
|
|
+ const ws = await apiClient.get<Workspace>(`/workspaces/${workspaceId}`)
|
|
|
+ setWorkspace(ws)
|
|
|
+
|
|
|
+ // Initialize form state from workspace
|
|
|
+ setName(ws.name)
|
|
|
+ const settings = ws.settings as WorkspaceSettings
|
|
|
+ setSelectedProviderId(settings?.llm_provider_id || '')
|
|
|
+ setSelectedModelId(settings?.llm_model_id || '')
|
|
|
+ setHelperProviderId(settings?.llm_helper_provider_id || '')
|
|
|
+ setHelperModelId(settings?.llm_helper_model_id || '')
|
|
|
+ setCompactionThreshold(settings?.llm_context_compaction_threshold ?? 80)
|
|
|
+ setSystemPrompt(settings?.llm_system_prompt || DEFAULT_SYSTEM_PROMPT)
|
|
|
+ setHelperSystemPrompt(settings?.llm_helper_system_prompt || DEFAULT_HELPER_SYSTEM_PROMPT)
|
|
|
+ } catch (err) {
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to load workspace')
|
|
|
+ } finally {
|
|
|
+ setIsLoading(false)
|
|
|
+ }
|
|
|
+ }, [workspaceId])
|
|
|
+
|
|
|
+ // Load providers
|
|
|
+ useEffect(() => {
|
|
|
+ const loadProviders = async () => {
|
|
|
+ try {
|
|
|
+ const response = await chatApi.listProviders()
|
|
|
+ setProviders(response.providers || [])
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load providers:', err)
|
|
|
+ } finally {
|
|
|
+ setIsLoadingProviders(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ loadProviders()
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // Load workspace after providers are loaded
|
|
|
+ useEffect(() => {
|
|
|
+ if (!isLoadingProviders) {
|
|
|
+ loadWorkspace()
|
|
|
+ }
|
|
|
+ }, [isLoadingProviders, loadWorkspace])
|
|
|
+
|
|
|
+ // Load models when provider changes
|
|
|
+ useEffect(() => {
|
|
|
+ if (!selectedProviderId) {
|
|
|
+ setModels([])
|
|
|
+ if (initialLoadDone) {
|
|
|
+ setSelectedModelId('')
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const loadModels = async () => {
|
|
|
+ setIsLoadingModels(true)
|
|
|
+ try {
|
|
|
+ const response = await chatApi.listModels(selectedProviderId)
|
|
|
+ setModels(response.models || [])
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load models:', err)
|
|
|
+ setModels([])
|
|
|
+ } finally {
|
|
|
+ setIsLoadingModels(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ loadModels()
|
|
|
+ }, [selectedProviderId, initialLoadDone])
|
|
|
+
|
|
|
+ // Load helper models when helper provider changes
|
|
|
+ // Note: When helperProviderId is empty, we use the primary provider's models
|
|
|
+ useEffect(() => {
|
|
|
+ if (!helperProviderId) {
|
|
|
+ // Use primary provider's models for helper when no separate provider selected
|
|
|
+ setHelperModels([])
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const loadHelperModels = async () => {
|
|
|
+ setIsLoadingHelperModels(true)
|
|
|
+ try {
|
|
|
+ const response = await chatApi.listModels(helperProviderId)
|
|
|
+ setHelperModels(response.models || [])
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load helper models:', err)
|
|
|
+ setHelperModels([])
|
|
|
+ } finally {
|
|
|
+ setIsLoadingHelperModels(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ loadHelperModels()
|
|
|
+ }, [helperProviderId])
|
|
|
+
|
|
|
+ // Mark initial load as done after workspace is loaded
|
|
|
+ useEffect(() => {
|
|
|
+ if (workspace && !isLoading) {
|
|
|
+ // Small delay to ensure models are loaded before allowing changes
|
|
|
+ const timer = setTimeout(() => setInitialLoadDone(true), 500)
|
|
|
+ return () => clearTimeout(timer)
|
|
|
+ }
|
|
|
+ }, [workspace, isLoading])
|
|
|
+
|
|
|
+ const handleSave = async () => {
|
|
|
+ if (!workspaceId) return
|
|
|
+
|
|
|
+ setIsSaving(true)
|
|
|
+ setError(null)
|
|
|
+ setSuccessMessage(null)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const settings: WorkspaceSettings = {
|
|
|
+ ...(workspace?.settings as WorkspaceSettings),
|
|
|
+ llm_provider_id: selectedProviderId || undefined,
|
|
|
+ llm_model_id: selectedModelId || undefined,
|
|
|
+ llm_helper_provider_id: helperProviderId || undefined,
|
|
|
+ llm_helper_model_id: helperModelId || undefined,
|
|
|
+ llm_context_compaction_threshold: compactionThreshold,
|
|
|
+ llm_system_prompt: systemPrompt,
|
|
|
+ llm_helper_system_prompt: helperSystemPrompt,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Remove undefined values
|
|
|
+ Object.keys(settings).forEach(key => {
|
|
|
+ if (settings[key] === undefined) {
|
|
|
+ delete settings[key]
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ await apiClient.patch(`/workspaces/${workspaceId}`, {
|
|
|
+ name,
|
|
|
+ settings,
|
|
|
+ })
|
|
|
+
|
|
|
+ setSuccessMessage('Workspace saved successfully')
|
|
|
+ refreshWorkspaces()
|
|
|
+
|
|
|
+ // Clear success message after 3 seconds
|
|
|
+ setTimeout(() => setSuccessMessage(null), 3000)
|
|
|
+ } catch (err) {
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to save workspace')
|
|
|
+ } finally {
|
|
|
+ setIsSaving(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleTestPrompt = async () => {
|
|
|
+ // Test the system prompt by executing it with mock context
|
|
|
+ try {
|
|
|
+ // Create a function from the code
|
|
|
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
|
|
|
+ const fn = new AsyncFunction('context', `
|
|
|
+ ${systemPrompt}
|
|
|
+ return generateSystemPrompt(context);
|
|
|
+ `)
|
|
|
+
|
|
|
+ const mockContext = {
|
|
|
+ user: {
|
|
|
+ id: user?.id || 'test-user',
|
|
|
+ name: user?.name || 'Test User',
|
|
|
+ email: user?.email || 'test@example.com',
|
|
|
+ },
|
|
|
+ workspace: {
|
|
|
+ id: workspace?.id || 'test-workspace',
|
|
|
+ name: workspace?.name || 'Test Workspace',
|
|
|
+ settings: workspace?.settings || {},
|
|
|
+ },
|
|
|
+ storage: {
|
|
|
+ query: async (collection: string, options: Record<string, unknown>) => {
|
|
|
+ console.log(`Mock storage query: ${collection}`, options)
|
|
|
+ return { documents: [], total: 0 }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await fn(mockContext)
|
|
|
+ setTestPromptResult({ success: true, content: result })
|
|
|
+ } catch (err) {
|
|
|
+ setTestPromptResult({ success: false, content: err instanceof Error ? err.message : 'Unknown error' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleTestHelperPrompt = async () => {
|
|
|
+ // Test the helper system prompt with different task contexts
|
|
|
+ try {
|
|
|
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
|
|
|
+ const fn = new AsyncFunction('context', `
|
|
|
+ ${helperSystemPrompt}
|
|
|
+ return generateSystemPrompt(context);
|
|
|
+ `)
|
|
|
+
|
|
|
+ const results: string[] = []
|
|
|
+
|
|
|
+ // Test with title_generation task
|
|
|
+ const titleContext = {
|
|
|
+ user: {
|
|
|
+ id: user?.id || 'test-user',
|
|
|
+ name: user?.name || 'Test User',
|
|
|
+ email: user?.email || 'test@example.com',
|
|
|
+ },
|
|
|
+ workspace: {
|
|
|
+ id: workspace?.id || 'test-workspace',
|
|
|
+ name: workspace?.name || 'Test Workspace',
|
|
|
+ settings: workspace?.settings || {},
|
|
|
+ },
|
|
|
+ task: 'title_generation',
|
|
|
+ }
|
|
|
+ const titleResult = await fn(titleContext)
|
|
|
+ results.push(`=== Task: title_generation ===\n${titleResult}`)
|
|
|
+
|
|
|
+ // Test with compression task
|
|
|
+ const compressionContext = { ...titleContext, task: 'compression' }
|
|
|
+ const compressionResult = await fn(compressionContext)
|
|
|
+ results.push(`\n=== Task: compression ===\n${compressionResult}`)
|
|
|
+
|
|
|
+ setTestPromptResult({ success: true, content: results.join('\n') })
|
|
|
+ } catch (err) {
|
|
|
+ setTestPromptResult({ success: false, content: err instanceof Error ? err.message : 'Unknown error' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return (
|
|
|
+ <div className="flex items-center justify-center py-12">
|
|
|
+ <svg
|
|
|
+ className="h-8 w-8 animate-spin text-primary-600"
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ fill="none"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ >
|
|
|
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
|
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!workspace) {
|
|
|
+ return (
|
|
|
+ <div className="text-center py-12">
|
|
|
+ <p className="text-gray-500">Workspace not found</p>
|
|
|
+ <Button className="mt-4" onClick={() => navigate('/workspaces')}>
|
|
|
+ Back to Workspaces
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* Header */}
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-bold text-gray-900">Edit Workspace</h1>
|
|
|
+ <p className="mt-1 text-gray-600">Configure workspace settings and AI assistant</p>
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-3">
|
|
|
+ <Button variant="secondary" onClick={() => navigate('/workspaces')}>
|
|
|
+ Cancel
|
|
|
+ </Button>
|
|
|
+ <Button onClick={handleSave} isLoading={isSaving}>
|
|
|
+ Save Changes
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Messages */}
|
|
|
+ {error && (
|
|
|
+ <div className="rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>
|
|
|
+ )}
|
|
|
+ {successMessage && (
|
|
|
+ <div className="rounded-lg bg-green-50 px-4 py-3 text-sm text-green-700">{successMessage}</div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
+ {/* Left Column - Basic Settings */}
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* Basic Info */}
|
|
|
+ <div className="rounded-lg bg-white p-6 shadow">
|
|
|
+ <h2 className="text-lg font-semibold text-gray-900 mb-4">Basic Information</h2>
|
|
|
+ <Input
|
|
|
+ label="Workspace Name"
|
|
|
+ value={name}
|
|
|
+ onChange={(e) => setName(e.target.value)}
|
|
|
+ placeholder="Enter workspace name"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* LLM Settings */}
|
|
|
+ <div className="rounded-lg bg-white p-6 shadow">
|
|
|
+ <h2 className="text-lg font-semibold text-gray-900 mb-4">AI Assistant Settings</h2>
|
|
|
+
|
|
|
+ {/* Primary Provider */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ LLM Provider
|
|
|
+ </label>
|
|
|
+ <select
|
|
|
+ value={selectedProviderId}
|
|
|
+ onChange={(e) => {
|
|
|
+ setSelectedProviderId(e.target.value)
|
|
|
+ if (initialLoadDone) {
|
|
|
+ setSelectedModelId('')
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ disabled={isLoadingProviders}
|
|
|
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 disabled:bg-gray-100"
|
|
|
+ >
|
|
|
+ <option value="">
|
|
|
+ {isLoadingProviders ? 'Loading providers...' : 'Select a provider'}
|
|
|
+ </option>
|
|
|
+ {providers.filter(p => p.enabled).map((provider) => (
|
|
|
+ <option key={provider.id} value={provider.id}>
|
|
|
+ {provider.name} ({provider.type})
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Primary Model */}
|
|
|
+ {selectedProviderId && (
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ Primary Model
|
|
|
+ </label>
|
|
|
+ <ModelSelector
|
|
|
+ models={models}
|
|
|
+ providers={providers}
|
|
|
+ selectedModelId={selectedModelId}
|
|
|
+ onChange={setSelectedModelId}
|
|
|
+ isLoading={isLoadingModels}
|
|
|
+ placeholder="Select a model"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Helper Model Section */}
|
|
|
+ {selectedProviderId && selectedModelId && (
|
|
|
+ <div className="border-t border-gray-200 pt-4 mt-4">
|
|
|
+ <h3 className="text-sm font-medium text-gray-900 mb-3">Helper Model</h3>
|
|
|
+ <p className="text-xs text-gray-500 mb-3">
|
|
|
+ A smaller, faster model for conversation compression and title generation.
|
|
|
+ </p>
|
|
|
+
|
|
|
+ {/* Helper Provider */}
|
|
|
+ <div className="mb-3">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ Helper Provider
|
|
|
+ </label>
|
|
|
+ <select
|
|
|
+ value={helperProviderId}
|
|
|
+ onChange={(e) => {
|
|
|
+ const newProviderId = e.target.value
|
|
|
+ setHelperProviderId(newProviderId)
|
|
|
+ // Only clear model when switching TO a different provider (not when going back to "same as primary")
|
|
|
+ if (initialLoadDone && newProviderId && newProviderId !== helperProviderId) {
|
|
|
+ setHelperModelId('')
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
|
+ >
|
|
|
+ <option value="">Same as primary provider</option>
|
|
|
+ {providers.filter(p => p.enabled).map((provider) => (
|
|
|
+ <option key={provider.id} value={provider.id}>
|
|
|
+ {provider.name} ({provider.type})
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Helper Model */}
|
|
|
+ <div className="mb-3">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ Helper Model
|
|
|
+ </label>
|
|
|
+ <ModelSelector
|
|
|
+ models={helperProviderId ? helperModels : models}
|
|
|
+ providers={providers}
|
|
|
+ selectedModelId={helperModelId}
|
|
|
+ onChange={setHelperModelId}
|
|
|
+ isLoading={helperProviderId ? isLoadingHelperModels : isLoadingModels}
|
|
|
+ placeholder="Select a smaller model"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Compaction Threshold */}
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ Auto-compress at {compactionThreshold}% context usage
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="range"
|
|
|
+ min="50"
|
|
|
+ max="95"
|
|
|
+ step="5"
|
|
|
+ value={compactionThreshold}
|
|
|
+ onChange={(e) => setCompactionThreshold(Number(e.target.value))}
|
|
|
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
|
|
+ />
|
|
|
+ <div className="flex justify-between text-xs text-gray-500 mt-1">
|
|
|
+ <span>50%</span>
|
|
|
+ <span>95%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Right Column - System Prompt Editors */}
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* Primary System Prompt Editor */}
|
|
|
+ <div className="rounded-lg bg-white p-6 shadow">
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
+ <div>
|
|
|
+ <h2 className="text-lg font-semibold text-gray-900">Primary Model System Prompt</h2>
|
|
|
+ <p className="text-xs text-gray-500 mt-1">
|
|
|
+ JavaScript function that generates the system prompt for the main AI assistant
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <Button variant="secondary" size="sm" onClick={handleTestPrompt}>
|
|
|
+ Test Prompt
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="border border-gray-300 rounded-lg overflow-hidden">
|
|
|
+ <Editor
|
|
|
+ height="400px"
|
|
|
+ defaultLanguage="javascript"
|
|
|
+ value={systemPrompt}
|
|
|
+ onChange={(value) => setSystemPrompt(value || '')}
|
|
|
+ theme="vs-light"
|
|
|
+ options={{
|
|
|
+ minimap: { enabled: false },
|
|
|
+ fontSize: 13,
|
|
|
+ lineNumbers: 'on',
|
|
|
+ scrollBeyondLastLine: false,
|
|
|
+ wordWrap: 'on',
|
|
|
+ tabSize: 2,
|
|
|
+ automaticLayout: true,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="mt-4 p-4 bg-gray-50 rounded-lg">
|
|
|
+ <h3 className="text-sm font-medium text-gray-900 mb-2">Available Context</h3>
|
|
|
+ <div className="text-xs text-gray-600 space-y-1 font-mono">
|
|
|
+ <p><span className="text-blue-600">context.user</span> - Current user (id, name, email)</p>
|
|
|
+ <p><span className="text-blue-600">context.workspace</span> - Workspace info (id, name, settings)</p>
|
|
|
+ <p><span className="text-blue-600">context.storage.query(collection, options)</span> - Query documents</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Helper Model System Prompt Editor */}
|
|
|
+ <div className="rounded-lg bg-white p-6 shadow">
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
+ <div>
|
|
|
+ <h2 className="text-lg font-semibold text-gray-900">Helper Model System Prompt</h2>
|
|
|
+ <p className="text-xs text-gray-500 mt-1">
|
|
|
+ Used for title generation and conversation compression (uses smaller/faster model)
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <Button variant="secondary" size="sm" onClick={handleTestHelperPrompt}>
|
|
|
+ Test Prompt
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="border border-gray-300 rounded-lg overflow-hidden">
|
|
|
+ <Editor
|
|
|
+ height="300px"
|
|
|
+ defaultLanguage="javascript"
|
|
|
+ value={helperSystemPrompt}
|
|
|
+ onChange={(value) => setHelperSystemPrompt(value || '')}
|
|
|
+ theme="vs-light"
|
|
|
+ options={{
|
|
|
+ minimap: { enabled: false },
|
|
|
+ fontSize: 13,
|
|
|
+ lineNumbers: 'on',
|
|
|
+ scrollBeyondLastLine: false,
|
|
|
+ wordWrap: 'on',
|
|
|
+ tabSize: 2,
|
|
|
+ automaticLayout: true,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="mt-4 p-4 bg-gray-50 rounded-lg">
|
|
|
+ <h3 className="text-sm font-medium text-gray-900 mb-2">Available Context</h3>
|
|
|
+ <div className="text-xs text-gray-600 space-y-1 font-mono">
|
|
|
+ <p><span className="text-blue-600">context.user</span> - Current user (id, name, email)</p>
|
|
|
+ <p><span className="text-blue-600">context.workspace</span> - Workspace info (id, name, settings)</p>
|
|
|
+ <p><span className="text-blue-600">context.task</span> - 'title_generation' | 'compression'</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Test Prompt Result Modal */}
|
|
|
+ {testPromptResult && (
|
|
|
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
|
+ <div className="w-full max-w-2xl max-h-[80vh] overflow-hidden rounded-lg bg-white shadow-xl flex flex-col">
|
|
|
+ <div className={`px-6 py-4 border-b ${testPromptResult.success ? 'bg-green-50' : 'bg-red-50'}`}>
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <h3 className={`text-lg font-semibold ${testPromptResult.success ? 'text-green-900' : 'text-red-900'}`}>
|
|
|
+ {testPromptResult.success ? 'Generated System Prompt' : 'Error in System Prompt'}
|
|
|
+ </h3>
|
|
|
+ <button
|
|
|
+ onClick={() => setTestPromptResult(null)}
|
|
|
+ className="text-gray-400 hover:text-gray-600"
|
|
|
+ >
|
|
|
+ <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="flex-1 overflow-y-auto p-6">
|
|
|
+ <pre className={`whitespace-pre-wrap text-sm font-mono p-4 rounded-lg ${
|
|
|
+ testPromptResult.success ? 'bg-gray-50 text-gray-800' : 'bg-red-50 text-red-800'
|
|
|
+ }`}>
|
|
|
+ {testPromptResult.content}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ <div className="px-6 py-4 border-t bg-gray-50 flex justify-end">
|
|
|
+ <Button variant="secondary" onClick={() => setTestPromptResult(null)}>
|
|
|
+ Close
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export default WorkspaceEdit
|