feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
315
product-source/hololake-platform/src/components/AiPanel.tsx
Normal file
315
product-source/hololake-platform/src/components/AiPanel.tsx
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
import { useCallback, useRef, type CSSProperties, type ReactNode, type RefObject } from 'react'
|
||||
import {
|
||||
AiPanelComposer,
|
||||
AiPanelHeader,
|
||||
AiPanelMessageHistory,
|
||||
} from './AiPanelChrome'
|
||||
import {
|
||||
DEFAULT_AI_AGENT,
|
||||
getAiAgentDefinition,
|
||||
type AiAgentId,
|
||||
type AiAgentReadiness,
|
||||
} from '../lib/aiAgents'
|
||||
import type { AiTarget } from '../lib/aiTargets'
|
||||
import type { AppLocale } from '../lib/i18n'
|
||||
import { type NoteListItem } from '../utils/ai-context'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { useAiPanelController, type AiPanelController } from './useAiPanelController'
|
||||
import { useAiPanelPromptQueue } from './useAiPanelPromptQueue'
|
||||
import { useAiPanelFocus } from './useAiPanelFocus'
|
||||
|
||||
export type { AiAgentMessage } from '../hooks/useCliAiAgent'
|
||||
|
||||
interface AiPanelProps {
|
||||
onClose: () => void
|
||||
onOpenNote?: (path: string) => void
|
||||
onUnsupportedAiPaste?: (message: string) => void
|
||||
defaultAiAgent?: AiAgentId
|
||||
defaultAiTarget?: AiTarget
|
||||
defaultAiAgentReadiness?: AiAgentReadiness
|
||||
defaultAiAgentReady?: boolean
|
||||
locale?: AppLocale
|
||||
onFileCreated?: (relativePath: string) => void
|
||||
onFileModified?: (relativePath: string) => void
|
||||
onVaultChanged?: () => void
|
||||
vaultPath: string
|
||||
vaultPaths?: string[]
|
||||
activeEntry?: VaultEntry | null
|
||||
/** Direct content of the active note from the editor tab. */
|
||||
activeNoteContent?: string | null
|
||||
entries?: VaultEntry[]
|
||||
openTabs?: VaultEntry[]
|
||||
noteList?: NoteListItem[]
|
||||
noteListFilter?: { type: string | null; query: string }
|
||||
}
|
||||
|
||||
interface AiPanelViewProps {
|
||||
controller: AiPanelController
|
||||
onClose: () => void
|
||||
onOpenNote?: (path: string) => void
|
||||
onUnsupportedAiPaste?: (message: string) => void
|
||||
defaultAiAgent?: AiAgentId
|
||||
defaultAiTarget?: AiTarget
|
||||
defaultAiAgentReadiness?: AiAgentReadiness
|
||||
defaultAiAgentReady?: boolean
|
||||
locale?: AppLocale
|
||||
activeEntry?: VaultEntry | null
|
||||
entries?: VaultEntry[]
|
||||
interactive?: boolean
|
||||
showHeader?: boolean
|
||||
showLeftBorder?: boolean
|
||||
surface?: 'default' | 'sidebar'
|
||||
composerControls?: ReactNode
|
||||
onForkMessage?: (messageId: string) => void
|
||||
onQueuedPromptTarget?: (targetId: string) => void
|
||||
onSendPrompt?: (text: string) => void
|
||||
onMessageHistoryScrollStateChange?: (scrolled: boolean) => void
|
||||
targetId?: string
|
||||
}
|
||||
|
||||
function readinessFromReadyFlag(ready: boolean | undefined): AiAgentReadiness {
|
||||
return (ready ?? true) ? 'ready' : 'missing'
|
||||
}
|
||||
|
||||
interface AiPanelViewModel {
|
||||
agentLabel: string
|
||||
defaultAiAgent: AiAgentId
|
||||
defaultAiAgentReadiness: AiAgentReadiness
|
||||
targetKind: AiTarget['kind']
|
||||
}
|
||||
|
||||
function resolveAiPanelViewModel({
|
||||
defaultAiAgent,
|
||||
defaultAiAgentReadiness,
|
||||
defaultAiAgentReady,
|
||||
defaultAiTarget,
|
||||
}: {
|
||||
defaultAiAgent?: AiAgentId
|
||||
defaultAiAgentReadiness?: AiAgentReadiness
|
||||
defaultAiAgentReady?: boolean
|
||||
defaultAiTarget?: AiTarget
|
||||
}): AiPanelViewModel {
|
||||
const resolvedAgent = defaultAiAgent ?? DEFAULT_AI_AGENT
|
||||
const resolvedReadiness = defaultAiAgentReadiness ?? readinessFromReadyFlag(defaultAiAgentReady)
|
||||
|
||||
return {
|
||||
agentLabel: defaultAiTarget?.label ?? getAiAgentDefinition(resolvedAgent).label,
|
||||
defaultAiAgent: resolvedAgent,
|
||||
defaultAiAgentReadiness: resolvedReadiness,
|
||||
targetKind: defaultAiTarget?.kind ?? 'agent',
|
||||
}
|
||||
}
|
||||
|
||||
function aiPanelFrameStyle(isActive: boolean, showLeftBorder: boolean): CSSProperties {
|
||||
return {
|
||||
outline: 'none',
|
||||
borderLeft: showLeftBorder
|
||||
? isActive
|
||||
? '2px solid var(--accent-blue)'
|
||||
: '1px solid var(--border)'
|
||||
: undefined,
|
||||
animation: showLeftBorder && isActive ? 'ai-border-pulse 2s ease-in-out infinite' : undefined,
|
||||
transition: showLeftBorder ? 'border-color 0.3s ease' : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function AiPanelFrame({
|
||||
children,
|
||||
isActive,
|
||||
panelRef,
|
||||
showLeftBorder,
|
||||
surface,
|
||||
}: {
|
||||
children: ReactNode
|
||||
isActive: boolean
|
||||
panelRef: RefObject<HTMLElement | null>
|
||||
showLeftBorder: boolean
|
||||
surface: 'default' | 'sidebar'
|
||||
}) {
|
||||
return (
|
||||
<aside
|
||||
ref={panelRef}
|
||||
tabIndex={-1}
|
||||
className={`flex flex-1 flex-col overflow-hidden ${surface === 'sidebar' ? 'bg-sidebar text-sidebar-foreground' : 'bg-background text-foreground'}`}
|
||||
style={aiPanelFrameStyle(isActive, showLeftBorder)}
|
||||
data-testid="ai-panel"
|
||||
data-ai-active={isActive || undefined}
|
||||
>
|
||||
{children}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export function AiPanelView({
|
||||
controller,
|
||||
onClose,
|
||||
onOpenNote,
|
||||
onUnsupportedAiPaste,
|
||||
defaultAiAgent: providedDefaultAiAgent,
|
||||
defaultAiTarget,
|
||||
defaultAiAgentReadiness: providedDefaultAiAgentReadiness,
|
||||
defaultAiAgentReady: providedDefaultAiAgentReady,
|
||||
locale = 'en',
|
||||
entries,
|
||||
interactive = true,
|
||||
showHeader = true,
|
||||
showLeftBorder = true,
|
||||
surface = 'default',
|
||||
composerControls,
|
||||
onForkMessage,
|
||||
onQueuedPromptTarget,
|
||||
onSendPrompt,
|
||||
onMessageHistoryScrollStateChange,
|
||||
targetId,
|
||||
}: AiPanelViewProps) {
|
||||
const view = resolveAiPanelViewModel({
|
||||
defaultAiAgent: providedDefaultAiAgent,
|
||||
defaultAiAgentReadiness: providedDefaultAiAgentReadiness,
|
||||
defaultAiAgentReady: providedDefaultAiAgentReady,
|
||||
defaultAiTarget,
|
||||
})
|
||||
const inputRef = useRef<HTMLDivElement>(null)
|
||||
const panelRef = useRef<HTMLElement>(null)
|
||||
const {
|
||||
agent,
|
||||
input,
|
||||
setInput,
|
||||
hasContext,
|
||||
isActive,
|
||||
permissionMode,
|
||||
handleSend,
|
||||
handleStop,
|
||||
handleNavigateWikilink,
|
||||
handlePermissionModeChange,
|
||||
handleNewChat,
|
||||
} = controller
|
||||
|
||||
useAiPanelPromptQueue({
|
||||
agent,
|
||||
currentTargetId: targetId,
|
||||
input,
|
||||
isActive,
|
||||
onTargetChange: onQueuedPromptTarget,
|
||||
setInput,
|
||||
enabled: interactive,
|
||||
})
|
||||
useAiPanelFocus({
|
||||
inputRef,
|
||||
panelRef,
|
||||
hasMessages: agent.messages.length > 0,
|
||||
isActive,
|
||||
onClose,
|
||||
enabled: interactive,
|
||||
})
|
||||
const handleComposerSend = useCallback((text: string, references: Parameters<typeof handleSend>[1]) => {
|
||||
if (!text.trim() || isActive) return
|
||||
onSendPrompt?.(text)
|
||||
handleSend(text, references)
|
||||
}, [handleSend, isActive, onSendPrompt])
|
||||
|
||||
return (
|
||||
<AiPanelFrame panelRef={panelRef} isActive={isActive} showLeftBorder={showLeftBorder} surface={surface}>
|
||||
{showHeader && (
|
||||
<AiPanelHeader
|
||||
agentLabel={view.agentLabel}
|
||||
agentReadiness={view.defaultAiAgentReadiness}
|
||||
targetKind={view.targetKind}
|
||||
locale={locale}
|
||||
permissionMode={permissionMode}
|
||||
permissionModeDisabled={isActive}
|
||||
onPermissionModeChange={handlePermissionModeChange}
|
||||
onClose={onClose}
|
||||
onNewChat={handleNewChat}
|
||||
/>
|
||||
)}
|
||||
<AiPanelMessageHistory
|
||||
agentLabel={view.agentLabel}
|
||||
agentReadiness={view.defaultAiAgentReadiness}
|
||||
locale={locale}
|
||||
messages={agent.messages}
|
||||
isActive={isActive}
|
||||
onForkMessage={onForkMessage}
|
||||
onOpenNote={onOpenNote}
|
||||
onNavigateWikilink={handleNavigateWikilink}
|
||||
onRegenerateMessage={agent.regenerateMessage}
|
||||
onScrollStateChange={onMessageHistoryScrollStateChange}
|
||||
hasContext={hasContext}
|
||||
/>
|
||||
<AiPanelComposer
|
||||
entries={entries ?? []}
|
||||
agentLabel={view.agentLabel}
|
||||
agentReadiness={view.defaultAiAgentReadiness}
|
||||
locale={locale}
|
||||
input={input}
|
||||
inputRef={inputRef}
|
||||
isActive={isActive}
|
||||
controls={composerControls}
|
||||
onChange={setInput}
|
||||
onSend={handleComposerSend}
|
||||
onStop={handleStop}
|
||||
onUnsupportedAiPaste={onUnsupportedAiPaste}
|
||||
/>
|
||||
</AiPanelFrame>
|
||||
)
|
||||
}
|
||||
|
||||
export function AiPanel({
|
||||
onClose,
|
||||
onOpenNote,
|
||||
onUnsupportedAiPaste,
|
||||
defaultAiAgent: providedDefaultAiAgent,
|
||||
defaultAiTarget,
|
||||
defaultAiAgentReadiness: providedDefaultAiAgentReadiness,
|
||||
defaultAiAgentReady: providedDefaultAiAgentReady,
|
||||
locale = 'en',
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
vaultPath,
|
||||
vaultPaths,
|
||||
activeEntry,
|
||||
activeNoteContent,
|
||||
entries,
|
||||
openTabs,
|
||||
noteList,
|
||||
noteListFilter,
|
||||
}: AiPanelProps) {
|
||||
const defaultAiAgentReadiness = providedDefaultAiAgentReadiness
|
||||
?? readinessFromReadyFlag(providedDefaultAiAgentReady)
|
||||
const controller = useAiPanelController({
|
||||
vaultPath,
|
||||
vaultPaths,
|
||||
defaultAiAgent: providedDefaultAiAgent ?? DEFAULT_AI_AGENT,
|
||||
defaultAiTarget,
|
||||
defaultAiAgentReady: providedDefaultAiAgentReady ?? true,
|
||||
defaultAiAgentReadiness,
|
||||
activeEntry,
|
||||
activeNoteContent,
|
||||
entries,
|
||||
openTabs,
|
||||
noteList,
|
||||
noteListFilter,
|
||||
locale,
|
||||
onOpenNote,
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
})
|
||||
|
||||
return (
|
||||
<AiPanelView
|
||||
controller={controller}
|
||||
onClose={onClose}
|
||||
onOpenNote={onOpenNote}
|
||||
onUnsupportedAiPaste={onUnsupportedAiPaste}
|
||||
defaultAiAgent={providedDefaultAiAgent}
|
||||
defaultAiTarget={defaultAiTarget}
|
||||
defaultAiAgentReadiness={defaultAiAgentReadiness}
|
||||
defaultAiAgentReady={providedDefaultAiAgentReady}
|
||||
locale={locale}
|
||||
activeEntry={activeEntry}
|
||||
entries={entries}
|
||||
targetId={defaultAiTarget?.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue