feat: add install readme et al
This commit is contained in:
@@ -46,13 +46,7 @@ export function ComposerCompletionDrawer({
|
||||
)
|
||||
}
|
||||
|
||||
export function CompletionDrawerEmpty({
|
||||
children,
|
||||
title
|
||||
}: {
|
||||
children?: ReactNode
|
||||
title: string
|
||||
}) {
|
||||
export function CompletionDrawerEmpty({ children, title }: { children?: ReactNode; title: string }) {
|
||||
return (
|
||||
<div className="px-3 py-3 text-sm text-muted-foreground">
|
||||
<p>{title}</p>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Clipboard, FileText, FolderOpen, ImageIcon, Link, type LucideIcon, MessageSquareText, Plus } from 'lucide-react'
|
||||
import {
|
||||
Clipboard,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
ImageIcon,
|
||||
Link,
|
||||
type LucideIcon,
|
||||
MessageSquareText,
|
||||
Plus
|
||||
} from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
|
||||
@@ -178,13 +178,7 @@ function ConversationIndicator({
|
||||
{bars.map((weight, index) => {
|
||||
const height = listening ? 0.3 + Math.min(0.7, normalized * weight) : 0.3
|
||||
|
||||
return (
|
||||
<span
|
||||
className="w-0.5 rounded-full bg-current"
|
||||
key={index}
|
||||
style={{ height: `${height * 100}%` }}
|
||||
/>
|
||||
)
|
||||
return <span className="w-0.5 rounded-full bg-current" key={index} style={{ height: `${height * 100}%` }} />
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
@@ -204,11 +198,7 @@ function DictationButton({
|
||||
const active = state.active || status !== 'idle'
|
||||
|
||||
const aria =
|
||||
status === 'recording'
|
||||
? 'Stop dictation'
|
||||
: status === 'transcribing'
|
||||
? 'Transcribing dictation'
|
||||
: 'Voice dictation'
|
||||
status === 'recording' ? 'Stop dictation' : status === 'transcribing' ? 'Transcribing dictation' : 'Voice dictation'
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { Unstable_TriggerAdapter, Unstable_TriggerItem } from '@assistant-ui/core'
|
||||
import { ComposerPrimitive, type Unstable_MentionDirective } from '@assistant-ui/react'
|
||||
|
||||
import {
|
||||
ComposerCompletionDrawer,
|
||||
CompletionDrawerEmpty,
|
||||
COMPLETION_DRAWER_ROW_CLASS
|
||||
} from './completion-drawer'
|
||||
import { COMPLETION_DRAWER_ROW_CLASS, CompletionDrawerEmpty, ComposerCompletionDrawer } from './completion-drawer'
|
||||
|
||||
export function DirectivePopover({
|
||||
adapter,
|
||||
@@ -44,11 +40,7 @@ function DirectiveRow({ index, item }: { index: number; item: Unstable_TriggerIt
|
||||
const description = metadata?.meta || item.description
|
||||
|
||||
return (
|
||||
<ComposerPrimitive.Unstable_TriggerPopoverItem
|
||||
className={COMPLETION_DRAWER_ROW_CLASS}
|
||||
index={index}
|
||||
item={item}
|
||||
>
|
||||
<ComposerPrimitive.Unstable_TriggerPopoverItem className={COMPLETION_DRAWER_ROW_CLASS} index={index} item={item}>
|
||||
<span className="shrink-0 truncate font-mono font-medium leading-5 text-foreground">{display}</span>
|
||||
{description && <span className="min-w-0 truncate leading-5 text-muted-foreground/80">{description}</span>}
|
||||
</ComposerPrimitive.Unstable_TriggerPopoverItem>
|
||||
|
||||
@@ -59,7 +59,9 @@ function Row({ description, keyLabel, mono = false }: { description: string; key
|
||||
return (
|
||||
<div className="flex min-w-0 items-baseline gap-2 rounded-md px-2.5 py-1 text-xs">
|
||||
<span
|
||||
className={mono ? 'shrink-0 truncate font-mono font-medium text-foreground/85' : 'shrink-0 truncate text-foreground/85'}
|
||||
className={
|
||||
mono ? 'shrink-0 truncate font-mono font-medium text-foreground/85' : 'shrink-0 truncate text-foreground/85'
|
||||
}
|
||||
>
|
||||
{keyLabel}
|
||||
</span>
|
||||
|
||||
@@ -181,6 +181,7 @@ export function useMicRecorder(): { handle: MicRecorderHandle; level: number; re
|
||||
['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg;codecs=opus', 'audio/ogg', 'audio/wav'].find(
|
||||
type => MediaRecorder.isTypeSupported(type)
|
||||
) ?? ''
|
||||
|
||||
let recorder: MediaRecorder
|
||||
|
||||
try {
|
||||
|
||||
@@ -38,9 +38,10 @@ function commandText(value: string): string {
|
||||
}
|
||||
|
||||
/** Live `/` completions backed by the gateway's `complete.slash` RPC. */
|
||||
export function useSlashCompletions(options: {
|
||||
gateway: HermesGateway | null
|
||||
}): { adapter: Unstable_TriggerAdapter; loading: boolean } {
|
||||
export function useSlashCompletions(options: { gateway: HermesGateway | null }): {
|
||||
adapter: Unstable_TriggerAdapter
|
||||
loading: boolean
|
||||
} {
|
||||
const { gateway } = options
|
||||
const enabled = Boolean(gateway)
|
||||
|
||||
|
||||
@@ -104,7 +104,11 @@ export function useVoiceConversation({
|
||||
}
|
||||
|
||||
if (!force && buffer.length > 220) {
|
||||
const softBoundary = Math.max(buffer.lastIndexOf(', ', 180), buffer.lastIndexOf('; ', 180), buffer.lastIndexOf(': ', 180))
|
||||
const softBoundary = Math.max(
|
||||
buffer.lastIndexOf(', ', 180),
|
||||
buffer.lastIndexOf('; ', 180),
|
||||
buffer.lastIndexOf(': ', 180)
|
||||
)
|
||||
|
||||
if (softBoundary > 80) {
|
||||
const chunk = buffer.slice(0, softBoundary + 1).trim()
|
||||
@@ -123,33 +127,21 @@ export function useVoiceConversation({
|
||||
return buffer
|
||||
}
|
||||
|
||||
const handleTurn = useCallback(async (forceTranscribe = false) => {
|
||||
if (turnClosingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
turnClosingRef.current = true
|
||||
clearTurnTimeout()
|
||||
setStatus('transcribing')
|
||||
|
||||
try {
|
||||
const result = await handle.stop()
|
||||
|
||||
if (!result || (!result.heardSpeech && !forceTranscribe) || !onTranscribeAudio) {
|
||||
if (enabledRef.current && !mutedRef.current && !busyRef.current && statusRef.current !== 'speaking') {
|
||||
pendingStartRef.current = true
|
||||
}
|
||||
|
||||
setStatus('idle')
|
||||
|
||||
const handleTurn = useCallback(
|
||||
async (forceTranscribe = false) => {
|
||||
if (turnClosingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const transcript = (await onTranscribeAudio(result.audio)).trim()
|
||||
turnClosingRef.current = true
|
||||
clearTurnTimeout()
|
||||
setStatus('transcribing')
|
||||
|
||||
if (!transcript) {
|
||||
if (enabledRef.current) {
|
||||
try {
|
||||
const result = await handle.stop()
|
||||
|
||||
if (!result || (!result.heardSpeech && !forceTranscribe) || !onTranscribeAudio) {
|
||||
if (enabledRef.current && !mutedRef.current && !busyRef.current && statusRef.current !== 'speaking') {
|
||||
pendingStartRef.current = true
|
||||
}
|
||||
|
||||
@@ -158,23 +150,38 @@ export function useVoiceConversation({
|
||||
return
|
||||
}
|
||||
|
||||
awaitingSpokenResponseRef.current = true
|
||||
resetSpeechBuffer()
|
||||
await onSubmit(transcript)
|
||||
setStatus('thinking')
|
||||
} catch (error) {
|
||||
notifyError(error, 'Voice transcription failed')
|
||||
try {
|
||||
const transcript = (await onTranscribeAudio(result.audio)).trim()
|
||||
|
||||
if (enabledRef.current && !mutedRef.current && !busyRef.current) {
|
||||
pendingStartRef.current = true
|
||||
if (!transcript) {
|
||||
if (enabledRef.current) {
|
||||
pendingStartRef.current = true
|
||||
}
|
||||
|
||||
setStatus('idle')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
awaitingSpokenResponseRef.current = true
|
||||
resetSpeechBuffer()
|
||||
await onSubmit(transcript)
|
||||
setStatus('thinking')
|
||||
} catch (error) {
|
||||
notifyError(error, 'Voice transcription failed')
|
||||
|
||||
if (enabledRef.current && !mutedRef.current && !busyRef.current) {
|
||||
pendingStartRef.current = true
|
||||
}
|
||||
|
||||
setStatus('idle')
|
||||
}
|
||||
|
||||
setStatus('idle')
|
||||
} finally {
|
||||
turnClosingRef.current = false
|
||||
}
|
||||
} finally {
|
||||
turnClosingRef.current = false
|
||||
}
|
||||
}, [handle, onSubmit, onTranscribeAudio])
|
||||
},
|
||||
[handle, onSubmit, onTranscribeAudio]
|
||||
)
|
||||
|
||||
const startListening = useCallback(async () => {
|
||||
pendingStartRef.current = false
|
||||
@@ -210,25 +217,22 @@ export function useVoiceConversation({
|
||||
}
|
||||
}, [handle, handleTurn, onFatalError])
|
||||
|
||||
const speak = useCallback(
|
||||
async (text: string) => {
|
||||
setStatus('speaking')
|
||||
const speak = useCallback(async (text: string) => {
|
||||
setStatus('speaking')
|
||||
|
||||
try {
|
||||
await playSpeechText(text, { source: 'voice-conversation' })
|
||||
} catch (error) {
|
||||
notifyError(error, 'Voice playback failed')
|
||||
} finally {
|
||||
if (enabledRef.current) {
|
||||
pendingStartRef.current = true
|
||||
setStatus('idle')
|
||||
} else {
|
||||
setStatus('idle')
|
||||
}
|
||||
try {
|
||||
await playSpeechText(text, { source: 'voice-conversation' })
|
||||
} catch (error) {
|
||||
notifyError(error, 'Voice playback failed')
|
||||
} finally {
|
||||
if (enabledRef.current) {
|
||||
pendingStartRef.current = true
|
||||
setStatus('idle')
|
||||
} else {
|
||||
setStatus('idle')
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const start = useCallback(async () => {
|
||||
if (!onTranscribeAudio) {
|
||||
|
||||
@@ -86,10 +86,7 @@ export function useVoiceRecorder({
|
||||
startedAtRef.current = Date.now()
|
||||
setElapsedSeconds(0)
|
||||
setVoiceStatus('recording')
|
||||
intervalRef.current = window.setInterval(
|
||||
() => setElapsedSeconds((Date.now() - startedAtRef.current) / 1000),
|
||||
250
|
||||
)
|
||||
intervalRef.current = window.setInterval(() => setElapsedSeconds((Date.now() - startedAtRef.current) / 1000), 250)
|
||||
const cap = Math.max(1, Math.min(Math.trunc(maxRecordingSeconds), 600))
|
||||
timeoutRef.current = window.setTimeout(() => void stop(), cap * 1000)
|
||||
} catch (error) {
|
||||
|
||||
@@ -378,10 +378,7 @@ export function ChatBar({
|
||||
loading={at.loading}
|
||||
/>
|
||||
<SlashPopover adapter={slash.adapter} loading={slash.loading} />
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{ background: glassTweaks.fadeBackground }}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0" style={{ background: glassTweaks.fadeBackground }} />
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className={cn(
|
||||
@@ -430,9 +427,7 @@ export function ChatBar({
|
||||
>
|
||||
<VoiceActivity state={voiceActivityState} />
|
||||
<VoicePlaybackActivity />
|
||||
{attachments.length > 0 && (
|
||||
<AttachmentList attachments={attachments} onRemove={onRemoveAttachment} />
|
||||
)}
|
||||
{attachments.length > 0 && <AttachmentList attachments={attachments} onRemove={onRemoveAttachment} />}
|
||||
{stacked ? (
|
||||
<>
|
||||
{input}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { Unstable_DirectiveFormatter, Unstable_TriggerAdapter, Unstable_TriggerItem } from '@assistant-ui/core'
|
||||
import { ComposerPrimitive } from '@assistant-ui/react'
|
||||
|
||||
import {
|
||||
ComposerCompletionDrawer,
|
||||
CompletionDrawerEmpty,
|
||||
COMPLETION_DRAWER_ROW_CLASS
|
||||
} from './completion-drawer'
|
||||
import { COMPLETION_DRAWER_ROW_CLASS, CompletionDrawerEmpty, ComposerCompletionDrawer } from './completion-drawer'
|
||||
|
||||
const slashFormatter: Unstable_DirectiveFormatter = {
|
||||
serialize(item: Unstable_TriggerItem): string {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { HermesGateway } from '@/hermes'
|
||||
|
||||
export interface ContextSuggestion {
|
||||
text: string
|
||||
display: string
|
||||
@@ -28,7 +30,7 @@ export interface ChatBarProps {
|
||||
focusKey?: string | null
|
||||
maxRecordingSeconds?: number
|
||||
state: ChatBarState
|
||||
gateway?: import('@/hermes').HermesGateway | null
|
||||
gateway?: HermesGateway | null
|
||||
sessionId?: string | null
|
||||
cwd?: string | null
|
||||
onCancel: () => void
|
||||
|
||||
@@ -2,7 +2,14 @@ import { Globe } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
const URL_HINT = /^https?:\/\//i
|
||||
|
||||
@@ -162,11 +162,7 @@ function PlaybackWaveform({ audioElement }: { audioElement: HTMLAudioElement | n
|
||||
return <canvas aria-hidden="true" className="block h-4 w-[88px]" ref={canvasRef} />
|
||||
}
|
||||
|
||||
export function VoiceActivity({
|
||||
state
|
||||
}: {
|
||||
state: VoiceActivityState
|
||||
}) {
|
||||
export function VoiceActivity({ state }: { state: VoiceActivityState }) {
|
||||
if (state.status === 'idle') {
|
||||
return null
|
||||
}
|
||||
@@ -194,7 +190,9 @@ export function VoiceActivity({
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="truncate font-medium text-foreground/85">{title}</span>
|
||||
<span className="font-mono text-[0.6875rem] text-muted-foreground/85">{formatElapsed(state.elapsedSeconds)}</span>
|
||||
<span className="font-mono text-[0.6875rem] text-muted-foreground/85">
|
||||
{formatElapsed(state.elapsedSeconds)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<VoiceLevelBars active={recording} level={state.level} />
|
||||
|
||||
@@ -2,11 +2,7 @@ import { useCallback } from 'react'
|
||||
|
||||
import { formatRefValue } from '@/components/assistant-ui/directive-text'
|
||||
import { attachmentId, contextPath, pathLabel } from '@/lib/chat-runtime'
|
||||
import {
|
||||
addComposerAttachment,
|
||||
type ComposerAttachment,
|
||||
removeComposerAttachment
|
||||
} from '@/store/composer'
|
||||
import { addComposerAttachment, type ComposerAttachment, removeComposerAttachment } from '@/store/composer'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
|
||||
import type { ImageAttachResponse, ImageDetachResponse } from '../../types'
|
||||
@@ -92,6 +88,7 @@ export function useComposerActions({ activeSessionId, currentCwd, requestGateway
|
||||
session_id: activeSessionId,
|
||||
path
|
||||
})
|
||||
|
||||
const attachedPath = result.path || path
|
||||
|
||||
if (result.attached) {
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
$gatewayState
|
||||
} from '@/store/session'
|
||||
|
||||
interface ChatRightRailProps
|
||||
extends Pick<React.ComponentProps<typeof SessionInspector>, 'onBrowseCwd' | 'onChangeCwd'> {
|
||||
interface ChatRightRailProps extends Pick<
|
||||
React.ComponentProps<typeof SessionInspector>,
|
||||
'onBrowseCwd' | 'onChangeCwd'
|
||||
> {
|
||||
onOpenModelPicker: () => void
|
||||
onSelectPersonality: (name: string) => void
|
||||
}
|
||||
|
||||
@@ -74,12 +74,16 @@ export function ChatSidebar({
|
||||
const sessionsLoading = useStore($sessionsLoading)
|
||||
const workingSessionIds = useStore($workingSessionIds)
|
||||
|
||||
const sortedSessions = useMemo(() => [...sessions].sort((a, b) => {
|
||||
const aTime = a.last_active || a.started_at || 0
|
||||
const bTime = b.last_active || b.started_at || 0
|
||||
const sortedSessions = useMemo(
|
||||
() =>
|
||||
[...sessions].sort((a, b) => {
|
||||
const aTime = a.last_active || a.started_at || 0
|
||||
const bTime = b.last_active || b.started_at || 0
|
||||
|
||||
return bTime - aTime
|
||||
}), [sessions])
|
||||
return bTime - aTime
|
||||
}),
|
||||
[sessions]
|
||||
)
|
||||
|
||||
const sessionsById = useMemo(() => new Map(sessions.map(session => [session.id, session])), [sessions])
|
||||
const workingSessionIdSet = useMemo(() => new Set(workingSessionIds), [workingSessionIds])
|
||||
|
||||
Reference in New Issue
Block a user