diff --git a/src/components/assistants/AssistantConfigView.tsx b/src/components/assistants/AssistantConfigView.tsx
new file mode 100644
index 0000000..357452c
--- /dev/null
+++ b/src/components/assistants/AssistantConfigView.tsx
@@ -0,0 +1,188 @@
+import { ChevronDown } from 'lucide-react'
+import { useDraftAssistant } from '../../context/DraftAssistantContext'
+import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcut'
+import { SHORTCUTS } from '../../constants/keyboard-shortcuts'
+
+const inputClassName = "w-full h-6 text-[11px] px-1.5 rounded-md border border-input bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
+const inputStyle = { boxShadow: 'inset 0 1px 2px 0 rgb(0 0 0 / 0.05)' }
+
+export function AssistantConfigView() {
+ const {
+ draftAssistant,
+ isEditing,
+ isSubmitting,
+ validationErrors,
+ updateDraft,
+ cancelDraft,
+ saveDraft,
+ } = useDraftAssistant()
+
+ const handleSave = () => {
+ if (draftAssistant) saveDraft()
+ }
+
+ // Keyboard shortcuts
+ useKeyboardShortcuts([
+ { shortcut: SHORTCUTS.SAVE, handler: handleSave, options: { skipInputs: false } },
+ { shortcut: SHORTCUTS.SAVE_ENTER, handler: handleSave, options: { skipInputs: false } },
+ { shortcut: SHORTCUTS.CANCEL, handler: cancelDraft },
+ ])
+
+ if (!draftAssistant) return null
+
+ const isNameValid = !validationErrors.name
+ const canSubmit = isEditing
+ ? !isSubmitting && isNameValid
+ : !isSubmitting && isNameValid && draftAssistant.name.trim().length > 0
+
+ return (
+
+ {/* Header */}
+
+
+ {isEditing ? 'Edit Assistant' : 'Create Assistant'}
+
+
+ {isEditing
+ ? 'Update assistant configuration'
+ : 'Configure a new Pinecone Assistant'}
+
+
+
+ {/* Configuration Form */}
+
+ {/* Form error */}
+ {validationErrors._form && (
+
+
{validationErrors._form}
+
+ )}
+
+ {/* Assistant Name */}
+
+
+
updateDraft({ name: e.target.value.toLowerCase() })}
+ placeholder="my-assistant"
+ className={inputClassName}
+ style={inputStyle}
+ autoFocus={!isEditing}
+ disabled={isEditing}
+ />
+ {validationErrors.name && (
+
{validationErrors.name}
+ )}
+ {!isEditing && (
+
+ 1-63 characters, lowercase letters, numbers, and hyphens only
+
+ )}
+
+
+ {/* Instructions */}
+
+
+ {/* Metadata */}
+
+
+ {/* Region */}
+
+
+
+
+
+
+ {isEditing && (
+
+ Region cannot be changed after creation
+
+ )}
+
+
+
+ {/* Footer Actions */}
+
+
+ {SHORTCUTS.SAVE_ENTER.keys} {isEditing ? 'save' : 'create'}
+ {' ยท '}
+ {SHORTCUTS.CANCEL.keys} cancel
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/layout/MainContent.tsx b/src/components/layout/MainContent.tsx
index e240152..e0a5305 100644
--- a/src/components/layout/MainContent.tsx
+++ b/src/components/layout/MainContent.tsx
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react'
import { useSelection } from '../../context/SelectionContext'
import { useDraftIndex } from '../../context/DraftIndexContext'
import { useDraftNamespace } from '../../context/DraftNamespaceContext'
+import { useDraftAssistant } from '../../context/DraftAssistantContext'
import { usePanel } from '../../context/PanelContext'
import { useEmbedding } from '../../context/EmbeddingContext'
import { usePinecone } from '../../providers/PineconeProvider'
@@ -11,6 +12,7 @@ import { IndexesPanel } from '../indexes/IndexesPanel'
import { NamespacesPanel } from '../namespaces/NamespacesPanel'
import { IndexConfigView } from '../indexes/IndexConfigView'
import { NamespaceConfigView } from '../namespaces/NamespaceConfigView'
+import { AssistantConfigView } from '../assistants/AssistantConfigView'
import VectorsView from '../vectors/VectorsView'
import VectorDetailPanel from '../vectors/VectorDetailPanel'
import { AssistantsPanel } from '../assistants/AssistantsPanel'
@@ -28,6 +30,7 @@ export function MainContent() {
const { activeIndex, activeNamespace } = useSelection()
const { draftIndex } = useDraftIndex()
const { draftNamespace } = useDraftNamespace()
+ const { draftAssistant, startCreation: startAssistantCreation, startEditing: startAssistantEditing } = useDraftAssistant()
const { currentProfile } = usePinecone()
const { mode } = useMode()
const { activeAssistant } = useAssistantSelection()
@@ -136,7 +139,11 @@ export function MainContent() {
data-testid={mode === 'assistant' ? 'assistants-panel' : 'indexes-panel'}
>
{mode === 'assistant' ? (
- setIndexesPanelOpen(false)} />
+ setIndexesPanelOpen(false)}
+ onCreateNew={startAssistantCreation}
+ onEditAssistant={startAssistantEditing}
+ />
) : (
setIndexesPanelOpen(false)} />
)}
@@ -158,8 +165,10 @@ export function MainContent() {
className="h-full transition-[padding] duration-200"
style={{ paddingLeft: `${leftPadding}px`, paddingRight: `${rightPadding}px` }}
>
- {/* Assistant mode - Chat View */}
- {mode === 'assistant' && activeAssistant ? (
+ {/* Assistant mode - Config View or Chat View */}
+ {mode === 'assistant' && draftAssistant ? (
+
+ ) : mode === 'assistant' && activeAssistant ? (
-
No vector selected
+
+ {mode === 'assistant' ? 'No file selected' : 'No vector selected'}
+
)}
diff --git a/src/context/DraftAssistantContext.tsx b/src/context/DraftAssistantContext.tsx
new file mode 100644
index 0000000..e05ab42
--- /dev/null
+++ b/src/context/DraftAssistantContext.tsx
@@ -0,0 +1,254 @@
+import { createContext, useContext, useState, useCallback, useMemo, useEffect, ReactNode } from 'react'
+import { usePinecone } from '../providers/PineconeProvider'
+import { useCreateAssistantMutation, useUpdateAssistantMutation, useAssistantDetailQuery } from '../hooks/useAssistantQueries'
+import { useAssistantSelection } from './AssistantSelectionContext'
+import type { AssistantModel } from '../../electron/types'
+
+export interface DraftAssistant {
+ name: string
+ instructions: string
+ metadata: string // JSON string for editing
+ region: 'us' | 'eu'
+}
+
+interface DraftAssistantContextValue {
+ draftAssistant: DraftAssistant | null
+ isEditing: boolean
+ editingAssistantName: string | null
+ isSubmitting: boolean
+ validationErrors: Record
+ startCreation: () => void
+ startEditing: (assistantName: string) => void
+ updateDraft: (updates: Partial) => void
+ cancelDraft: () => void
+ saveDraft: () => Promise
+}
+
+const DraftAssistantContext = createContext(null)
+
+// Validation constants
+const NAME_MIN_LENGTH = 1
+const NAME_MAX_LENGTH = 63
+const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/
+const INSTRUCTIONS_MAX_LENGTH = 16 * 1024 // 16KB
+
+// Helper: create initial draft with defaults
+function createInitialDraft(): DraftAssistant {
+ return {
+ name: '',
+ instructions: '',
+ metadata: '',
+ region: 'us',
+ }
+}
+
+// Helper: create draft from existing assistant
+function createDraftFromAssistant(assistant: AssistantModel): DraftAssistant {
+ return {
+ name: assistant.name,
+ instructions: assistant.instructions || '',
+ metadata: assistant.metadata ? JSON.stringify(assistant.metadata, null, 2) : '',
+ region: 'us', // Region is set at creation time; cannot be changed
+ }
+}
+
+// Helper: validate draft and return errors
+function validateDraft(draft: DraftAssistant, isEditing: boolean): Record {
+ const errors: Record = {}
+
+ // Name validation (only for create, not edit)
+ if (!isEditing) {
+ const name = draft.name.trim()
+ if (!name) {
+ errors.name = 'Name is required'
+ } else if (name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
+ errors.name = `Name must be ${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} characters`
+ } else if (!NAME_PATTERN.test(name)) {
+ errors.name = 'Name must be lowercase alphanumeric with hyphens (e.g., my-assistant)'
+ }
+ }
+
+ // Instructions validation
+ if (draft.instructions && draft.instructions.length > INSTRUCTIONS_MAX_LENGTH) {
+ errors.instructions = `Instructions must be under ${INSTRUCTIONS_MAX_LENGTH / 1024}KB`
+ }
+
+ // Metadata validation (must be valid JSON if provided)
+ if (draft.metadata.trim()) {
+ try {
+ const parsed = JSON.parse(draft.metadata)
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
+ errors.metadata = 'Metadata must be a JSON object'
+ } else {
+ // Check that all values are strings
+ for (const [key, value] of Object.entries(parsed)) {
+ if (typeof value !== 'string') {
+ errors.metadata = `Metadata value for "${key}" must be a string`
+ break
+ }
+ }
+ }
+ } catch {
+ errors.metadata = 'Invalid JSON format'
+ }
+ }
+
+ return errors
+}
+
+export function DraftAssistantProvider({ children }: { children: ReactNode }) {
+ const [draftAssistant, setDraftAssistant] = useState(null)
+ const [isEditing, setIsEditing] = useState(false)
+ const [editingAssistantName, setEditingAssistantName] = useState(null)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [validationErrors, setValidationErrors] = useState>({})
+
+ const { currentProfile } = usePinecone()
+ const { setActiveAssistant } = useAssistantSelection()
+ const createMutation = useCreateAssistantMutation(currentProfile?.id || '')
+ const updateMutation = useUpdateAssistantMutation(currentProfile?.id || '')
+
+ // Fetch assistant detail when editing
+ const { data: editingAssistant } = useAssistantDetailQuery(
+ currentProfile?.id || null,
+ editingAssistantName,
+ !!editingAssistantName
+ )
+
+ // Update draft when editing assistant data loads
+ useEffect(() => {
+ if (isEditing && editingAssistant && editingAssistantName) {
+ setDraftAssistant(createDraftFromAssistant(editingAssistant))
+ }
+ }, [isEditing, editingAssistant, editingAssistantName])
+
+ const startCreation = useCallback(() => {
+ setDraftAssistant(createInitialDraft())
+ setIsEditing(false)
+ setEditingAssistantName(null)
+ setValidationErrors({})
+ setActiveAssistant(null)
+ }, [setActiveAssistant])
+
+ const startEditing = useCallback((assistantName: string) => {
+ setEditingAssistantName(assistantName)
+ setIsEditing(true)
+ setValidationErrors({})
+ // Draft will be populated when editingAssistant data loads
+ setDraftAssistant({
+ name: assistantName,
+ instructions: '',
+ metadata: '',
+ region: 'us',
+ })
+ setActiveAssistant(null)
+ }, [setActiveAssistant])
+
+ const updateDraft = useCallback((updates: Partial) => {
+ setDraftAssistant(prev => prev ? { ...prev, ...updates } : prev)
+ // Clear validation errors for updated fields
+ const updatedKeys = Object.keys(updates)
+ if (updatedKeys.length > 0) {
+ setValidationErrors(prev => {
+ const next = { ...prev }
+ updatedKeys.forEach(key => delete next[key])
+ return next
+ })
+ }
+ }, [])
+
+ const cancelDraft = useCallback(() => {
+ setDraftAssistant(null)
+ setIsEditing(false)
+ setEditingAssistantName(null)
+ setValidationErrors({})
+ }, [])
+
+ const saveDraft = useCallback(async () => {
+ if (!draftAssistant || !currentProfile) return
+
+ // Validate
+ const errors = validateDraft(draftAssistant, isEditing)
+ if (Object.keys(errors).length > 0) {
+ setValidationErrors(errors)
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ // Parse metadata
+ let metadata: Record | undefined
+ if (draftAssistant.metadata.trim()) {
+ metadata = JSON.parse(draftAssistant.metadata)
+ }
+
+ if (isEditing && editingAssistantName) {
+ // Update existing assistant
+ await updateMutation.mutateAsync({
+ name: editingAssistantName,
+ params: {
+ instructions: draftAssistant.instructions || undefined,
+ metadata,
+ },
+ })
+ // Select the updated assistant
+ setActiveAssistant(editingAssistantName)
+ } else {
+ // Create new assistant
+ await createMutation.mutateAsync({
+ name: draftAssistant.name.trim(),
+ instructions: draftAssistant.instructions || undefined,
+ metadata,
+ region: draftAssistant.region,
+ })
+ // Select the new assistant
+ setActiveAssistant(draftAssistant.name.trim())
+ }
+
+ // Clear draft on success
+ setDraftAssistant(null)
+ setIsEditing(false)
+ setEditingAssistantName(null)
+ setValidationErrors({})
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Failed to save assistant'
+ setValidationErrors({ _form: message })
+ } finally {
+ setIsSubmitting(false)
+ }
+ }, [draftAssistant, currentProfile, isEditing, editingAssistantName, createMutation, updateMutation, setActiveAssistant])
+
+ const value = useMemo(() => ({
+ draftAssistant,
+ isEditing,
+ editingAssistantName,
+ isSubmitting,
+ validationErrors,
+ startCreation,
+ startEditing,
+ updateDraft,
+ cancelDraft,
+ saveDraft,
+ }), [
+ draftAssistant,
+ isEditing,
+ editingAssistantName,
+ isSubmitting,
+ validationErrors,
+ startCreation,
+ startEditing,
+ updateDraft,
+ cancelDraft,
+ saveDraft,
+ ])
+
+ return {children}
+}
+
+export function useDraftAssistant() {
+ const context = useContext(DraftAssistantContext)
+ if (!context) {
+ throw new Error('useDraftAssistant must be used within a DraftAssistantProvider')
+ }
+ return context
+}
diff --git a/src/windows/ConnectionWindow.tsx b/src/windows/ConnectionWindow.tsx
index 6a51d46..efd5871 100644
--- a/src/windows/ConnectionWindow.tsx
+++ b/src/windows/ConnectionWindow.tsx
@@ -4,6 +4,7 @@ import { QueryStateProvider } from '../context/QueryStateContext'
import { EmbeddingProvider } from '../context/EmbeddingContext'
import { DraftIndexProvider } from '../context/DraftIndexContext'
import { DraftNamespaceProvider } from '../context/DraftNamespaceContext'
+import { DraftAssistantProvider } from '../context/DraftAssistantContext'
import { ClipboardProvider } from '../context/ClipboardContext'
import { ModeProvider } from '../context/ModeContext'
import { AssistantSelectionProvider } from '../context/AssistantSelectionContext'
@@ -49,9 +50,11 @@ export function ConnectionWindow({ windowId, profileId }: ConnectionWindowProps)
-
-
-
+
+
+
+
+