diff --git a/portfolio-view/index.html b/portfolio-view/index.html
index 87f45a4..1acfa94 100644
--- a/portfolio-view/index.html
+++ b/portfolio-view/index.html
@@ -6,6 +6,9 @@
Alexander — Java / Fullstack Developer
+
+
+
diff --git a/portfolio-view/src/components/chat/ChatMessage.tsx b/portfolio-view/src/components/chat/ChatMessage.tsx
index c62f803..944ad8c 100644
--- a/portfolio-view/src/components/chat/ChatMessage.tsx
+++ b/portfolio-view/src/components/chat/ChatMessage.tsx
@@ -26,9 +26,6 @@ export function ChatMessage({ message }: ChatMessageProps) {
}`}
>
{message.content}
- {message.isStreaming && (
-
- )}
)
diff --git a/portfolio-view/src/components/chat/ChatWidget.tsx b/portfolio-view/src/components/chat/ChatWidget.tsx
index 13fcaa6..a0db4b1 100644
--- a/portfolio-view/src/components/chat/ChatWidget.tsx
+++ b/portfolio-view/src/components/chat/ChatWidget.tsx
@@ -1,7 +1,7 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import type { ChatMessageType } from '../../types'
import { useHrGuestAuth } from '../../hooks/useHrGuestAuth'
-import { createChat, streamChatQuery } from '../../services/api'
+import { createChat, sendChatQuery } from '../../services/api'
import { ChatMessage } from './ChatMessage'
import { ChatInput } from './ChatInput'
@@ -16,7 +16,6 @@ export function ChatWidget() {
const { token, isLoading: tokenLoading, error: tokenError } = useHrGuestAuth()
const messagesEndRef = useRef(null)
- const abortRef = useRef<(() => void) | null>(null)
// Auto-scroll to latest message
useEffect(() => {
@@ -45,64 +44,34 @@ export function ChatWidget() {
role: 'user',
content: text,
}
- const assistantMsgId = `a-${Date.now()}`
- const assistantMsg: ChatMessageType = {
- id: assistantMsgId,
- role: 'assistant',
- content: '',
- isStreaming: true,
- }
- setMessages((prev) => [...prev, userMsg, assistantMsg])
+ setMessages((prev) => [...prev, userMsg])
setIsSending(true)
- const abort = streamChatQuery(
- chatId,
- text,
- token,
- (chunk) => {
- setMessages((prev) =>
- prev.map((m) =>
- m.id === assistantMsgId
- ? { ...m, content: m.content + chunk }
- : m
- )
- )
- },
- () => {
- setMessages((prev) =>
- prev.map((m) =>
- m.id === assistantMsgId ? { ...m, isStreaming: false } : m
- )
- )
+ sendChatQuery(chatId, text, token)
+ .then((response) => {
+ const assistantMsg: ChatMessageType = {
+ id: `a-${response.id}`,
+ role: 'assistant',
+ content: response.content,
+ }
+ setMessages((prev) => [...prev, assistantMsg])
+ })
+ .catch((err: unknown) => {
+ const assistantMsg: ChatMessageType = {
+ id: `a-err-${Date.now()}`,
+ role: 'assistant',
+ content: `Error: ${err instanceof Error ? err.message : 'Unknown error'}`,
+ }
+ setMessages((prev) => [...prev, assistantMsg])
+ })
+ .finally(() => {
setIsSending(false)
- abortRef.current = null
- },
- (err) => {
- setMessages((prev) =>
- prev.map((m) =>
- m.id === assistantMsgId
- ? { ...m, content: `Error: ${err.message}`, isStreaming: false }
- : m
- )
- )
- setIsSending(false)
- abortRef.current = null
- }
- )
-
- abortRef.current = abort
+ })
},
[token, chatId, isSending]
)
- // Clean up on unmount
- useEffect(() => {
- return () => {
- abortRef.current?.()
- }
- }, [])
-
const isInputDisabled =
tokenLoading ||
!!tokenError ||
diff --git a/portfolio-view/src/index.css b/portfolio-view/src/index.css
index 4eaad2b..c626b98 100644
--- a/portfolio-view/src/index.css
+++ b/portfolio-view/src/index.css
@@ -1,5 +1,3 @@
-@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&family=JetBrains+Mono:wght@400;500&display=swap');
-
@import "tailwindcss";
@theme {
diff --git a/portfolio-view/src/services/api.ts b/portfolio-view/src/services/api.ts
index 37e5f17..4b412c9 100644
--- a/portfolio-view/src/services/api.ts
+++ b/portfolio-view/src/services/api.ts
@@ -27,83 +27,29 @@ export async function createChat(token: string): Promise {
return data.id
}
-/**
- * Sends a chat query and streams the SSE response token by token.
- * Returns a cleanup function that aborts the request.
- */
-export function streamChatQuery(
+export interface ChatEntryResponse {
+ id: number
+ content: string
+ role: string
+ createdAt: string
+}
+
+export async function sendChatQuery(
chatId: string,
message: string,
- token: string,
- onChunk: (chunk: string) => void,
- onDone: () => void,
- onError: (error: Error) => void
-): () => void {
- const controller = new AbortController()
-
- fetch(`${BASE_URL}/api/rag/entry/${chatId}`, {
+ token: string
+): Promise {
+ const response = await fetch(`${BASE_URL}/api/rag/entry/${chatId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
- Accept: 'text/event-stream',
+ Accept: 'application/json',
},
- body: JSON.stringify({ message }),
- signal: controller.signal,
+ body: JSON.stringify({ content: message, onlyContext: true }),
})
- .then(async (response) => {
- if (!response.ok) {
- throw new Error(`Query failed: ${response.status}`)
- }
- if (!response.body) {
- throw new Error('Empty response body')
- }
-
- const reader = response.body.getReader()
- const decoder = new TextDecoder()
- let buffer = ''
-
- while (true) {
- const { done, value } = await reader.read()
- if (done) {
- onDone()
- break
- }
-
- buffer += decoder.decode(value, { stream: true })
- const lines = buffer.split('\n')
- // Keep the last (possibly incomplete) line in the buffer
- buffer = lines.pop() ?? ''
-
- for (const line of lines) {
- if (!line.startsWith('data:')) continue
- const data = line.slice(5).trim()
- if (data === '[DONE]' || data === '') continue
-
- // Handle both plain text and JSON payloads: {"content": "..."}
- try {
- const parsed: unknown = JSON.parse(data)
- if (
- parsed !== null &&
- typeof parsed === 'object' &&
- 'content' in parsed &&
- typeof (parsed as Record).content === 'string'
- ) {
- onChunk((parsed as { content: string }).content)
- } else {
- onChunk(data)
- }
- } catch {
- onChunk(data)
- }
- }
- }
- })
- .catch((error: unknown) => {
- if (error instanceof Error && error.name !== 'AbortError') {
- onError(error)
- }
- })
-
- return () => controller.abort()
+ if (!response.ok) {
+ throw new Error(`Query failed: ${response.status}`)
+ }
+ return response.json() as Promise
}
diff --git a/portfolio-view/src/types/index.ts b/portfolio-view/src/types/index.ts
index ca409be..223e230 100644
--- a/portfolio-view/src/types/index.ts
+++ b/portfolio-view/src/types/index.ts
@@ -11,7 +11,6 @@ export interface ChatMessageType {
id: string
role: 'user' | 'assistant'
content: string
- isStreaming?: boolean
}
export interface HrGuestTokenResponse {