Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions apps/desktop/src/components/chat/context-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { X } from "lucide-react";

import { cn } from "@hypr/utils";

import { useShell } from "../../contexts/shell";
import type { ContextRef } from "../../contexts/shell/chat";
import { useSession } from "../../hooks/tinybase";

export function ContextBar() {
const { chat } = useShell();

if (chat.refs.length === 0) {
return null;
}

return (
<div className="flex gap-1 px-3 py-2 overflow-x-auto">
{chat.refs.map((ref) => (
<ContextChip
key={`${ref.type}-${ref.id}`}
contextRef={ref}
onRemove={() => chat.removeRef(ref.id)}
/>
))}
</div>
);
}

function ContextChip({
contextRef,
onRemove,
}: {
contextRef: ContextRef;
onRemove: () => void;
}) {
const title = useRefTitle(contextRef);

return (
<div
className={cn([
"flex items-center gap-1 px-2 py-1 rounded-md text-xs",
"bg-neutral-100 text-neutral-700",
"border border-neutral-200",
])}
>
<span className="truncate max-w-[120px]">{title}</span>
<button
onClick={onRemove}
className="text-neutral-400 hover:text-neutral-600 transition-colors"
title="Remove context"
>
<X size={12} />
</button>
</div>
);
}

function useRefTitle(ref: ContextRef): string {
const { title } = useSession(ref.type === "session" ? ref.id : "");

if (ref.type === "session") {
return (title as string) || "Untitled Session";
}

return ref.id;
}
15 changes: 7 additions & 8 deletions apps/desktop/src/components/chat/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ import * as main from "../../store/tinybase/store/main";
export function ChatMessageInput({
onSendMessage,
disabled: disabledProp,
attachedSession,
isStreaming,
onStop,
}: {
onSendMessage: (content: string, parts: any[]) => void;
disabled?: boolean | { disabled: boolean; message?: string };
attachedSession?: { id: string; title?: string };
isStreaming?: boolean;
onStop?: () => void;
}) {
Expand Down Expand Up @@ -148,23 +146,24 @@ export function ChatMessageInput({
id: rowId,
type: "session",
label: title,
content: "",
});
}
});

return results.slice(0, 5);
},
onSelect: (item) => {
if (item.type === "session") {
chat.addRef({ type: "session", id: item.id, source: "manual" });
}
},
}),
[chatShortcuts, sessions],
[chatShortcuts, sessions, chat],
);

return (
<Container>
{attachedSession && (
<div className="px-3 pt-2 text-xs text-neutral-500 truncate">
Attached: {attachedSession.title || "Untitled"}
</div>
)}
<div className="flex flex-col p-2">
<div className="flex-1 mb-2">
<ChatEditor
Expand Down
146 changes: 72 additions & 74 deletions apps/desktop/src/components/chat/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import type { ChatStatus } from "ai";
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";

import {
type ChatContext,
commands as templateCommands,
type Transcript,
} from "@hypr/plugin-template";
import type { ChatMessage, ChatMessageStorage } from "@hypr/store";

import { CustomChatTransport } from "../../chat/transport";
import type { HyprUIMessage } from "../../chat/types";
import type { ContextRef } from "../../contexts/shell/chat";
import { useToolRegistry } from "../../contexts/tool";
import { useSession } from "../../hooks/tinybase";
import { useLanguageModel } from "../../hooks/useLLMConnection";
import * as main from "../../store/tinybase/store/main";
import { id } from "../../utils";
Expand All @@ -24,7 +25,7 @@ import {
interface ChatSessionProps {
sessionId: string;
chatGroupId?: string;
attachedSessionId?: string;
contextRefs: ContextRef[];
children: (props: {
messages: HyprUIMessage[];
sendMessage: (message: HyprUIMessage) => void;
Expand All @@ -38,10 +39,10 @@ interface ChatSessionProps {
export function ChatSession({
sessionId,
chatGroupId,
attachedSessionId,
contextRefs,
children,
}: ChatSessionProps) {
const transport = useTransport(attachedSessionId);
const transport = useTransport(contextRefs);
const store = main.UI.useStore(main.STORE_ID);

const { user_id } = main.UI.useValues(main.STORE_ID);
Expand Down Expand Up @@ -179,95 +180,92 @@ export function ChatSession({
);
}

function useTransport(attachedSessionId?: string) {
function useTransport(contextRefs: ContextRef[]) {
const registry = useToolRegistry();
const model = useLanguageModel();
const store = main.UI.useStore(main.STORE_ID);
const indexes = main.UI.useIndexes(main.STORE_ID);
const language = main.UI.useValue("ai_language", main.STORE_ID) ?? "en";
const [systemPrompt, setSystemPrompt] = useState<string | undefined>();

const { title, rawMd, createdAt } = useSession(attachedSessionId ?? "");

const enhancedNoteIds = main.UI.useSliceRowIds(
main.INDEXES.enhancedNotesBySession,
attachedSessionId ?? "",
main.STORE_ID,
);
const firstEnhancedNoteId = enhancedNoteIds?.[0];
const enhancedContent = main.UI.useCell(
"enhanced_notes",
firstEnhancedNoteId ?? "",
"content",
main.STORE_ID,
);

const transcriptIds = main.UI.useSliceRowIds(
main.INDEXES.transcriptBySession,
attachedSessionId ?? "",
main.STORE_ID,
const sessionRefs = useMemo(
() => contextRefs.filter((ref) => ref.type === "session"),
[contextRefs],
);
const firstTranscriptId = transcriptIds?.[0];

const wordIds = main.UI.useSliceRowIds(
main.INDEXES.wordsByTranscript,
firstTranscriptId ?? "",
main.STORE_ID,
);

const words = useMemo((): WordLike[] => {
if (!store || !wordIds || wordIds.length === 0) {
return [];
}

const result: WordLike[] = [];

for (const wordId of wordIds) {
const row = store.getRow("words", wordId);
if (row) {
result.push({
text: row.text as string,
start_ms: row.start_ms as number,
end_ms: row.end_ms as number,
channel: row.channel as WordLike["channel"],
});
}
const chatContext = useMemo((): ChatContext | null => {
if (sessionRefs.length === 0 || !store || !indexes) {
return null;
}

return result.sort((a, b) => a.start_ms - b.start_ms);
}, [store, wordIds]);

const transcript = useMemo((): Transcript | null => {
if (words.length === 0 || !store) {
const firstSessionRef = sessionRefs[0];
const session = store.getRow("sessions", firstSessionRef.id);
if (!session) {
return null;
}

const segments = buildSegments(words, [], []);
const ctx = defaultRenderLabelContext(store);
const manager = SpeakerLabelManager.fromSegments(segments, ctx);

return {
segments: segments.map((seg) => ({
speaker: SegmentKey.renderLabel(seg.key, ctx, manager),
text: seg.words.map((w) => w.text).join(" "),
})),
startedAt: null,
endedAt: null,
};
}, [words, store]);
const enhancedNoteIds = indexes.getSliceRowIds(
main.INDEXES.enhancedNotesBySession,
firstSessionRef.id,
);
const firstEnhancedNoteId = enhancedNoteIds?.[0];
const enhancedNote = firstEnhancedNoteId
? store.getRow("enhanced_notes", firstEnhancedNoteId)
: null;

const transcriptIds = indexes.getSliceRowIds(
main.INDEXES.transcriptBySession,
firstSessionRef.id,
);
const firstTranscriptId = transcriptIds?.[0];

let transcript: Transcript | null = null;
if (firstTranscriptId) {
const wordIds = indexes.getSliceRowIds(
main.INDEXES.wordsByTranscript,
firstTranscriptId,
);

if (wordIds && wordIds.length > 0) {
const words: WordLike[] = [];
for (const wordId of wordIds) {
const row = store.getRow("words", wordId);
if (row) {
words.push({
text: row.text as string,
start_ms: row.start_ms as number,
end_ms: row.end_ms as number,
channel: row.channel as WordLike["channel"],
});
}
}

const chatContext = useMemo(() => {
if (!attachedSessionId) {
return null;
if (words.length > 0) {
words.sort((a, b) => a.start_ms - b.start_ms);
const segments = buildSegments(words, [], []);
const ctx = defaultRenderLabelContext(store);
const manager = SpeakerLabelManager.fromSegments(segments, ctx);

transcript = {
segments: segments.map((seg) => ({
speaker: SegmentKey.renderLabel(seg.key, ctx, manager),
text: seg.words.map((w) => w.text).join(" "),
})),
startedAt: null,
endedAt: null,
};
}
}
}

return {
title: (title as string) || null,
date: (createdAt as string) || null,
rawContent: (rawMd as string) || null,
enhancedContent: (enhancedContent as string) || null,
title: (session.title as string) || null,
date: (session.created_at as string) || null,
rawContent: (session.raw_md as string) || null,
enhancedContent: (enhancedNote?.content as string) || null,
transcript,
};
}, [attachedSessionId, title, rawMd, enhancedContent, createdAt, transcript]);
}, [sessionRefs, store, indexes]);

useEffect(() => {
templateCommands
Expand Down
Loading
Loading