Merge pull request 'fix(chat): restrict to authenticated users only, fix overlay transparency' (#672) from fix/chat-auth-only into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
This commit was merged in pull request #672.
This commit is contained in:
@@ -80,8 +80,8 @@
|
|||||||
"session_id": "sess-002",
|
"session_id": "sess-002",
|
||||||
"runtime": "unknown",
|
"runtime": "unknown",
|
||||||
"started_at": "2026-02-28T20:30:13Z",
|
"started_at": "2026-02-28T20:30:13Z",
|
||||||
"ended_at": "",
|
"ended_at": "2026-03-04T13:45:06Z",
|
||||||
"ended_reason": "",
|
"ended_reason": "completed",
|
||||||
"milestone_at_end": "",
|
"milestone_at_end": "",
|
||||||
"tasks_completed": [],
|
"tasks_completed": [],
|
||||||
"last_task_id": ""
|
"last_task_id": ""
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"session_id": "sess-002",
|
|
||||||
"runtime": "unknown",
|
|
||||||
"pid": 3178395,
|
|
||||||
"started_at": "2026-02-28T20:30:13Z",
|
|
||||||
"project_path": "/tmp/ms21-ui-001",
|
|
||||||
"milestone_id": ""
|
|
||||||
}
|
|
||||||
@@ -342,6 +342,31 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input Area */}
|
{/* Input Area */}
|
||||||
|
{!user && (
|
||||||
|
<div className="mx-4 mb-2 lg:mx-auto lg:max-w-4xl lg:px-8">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center gap-2 rounded-lg border px-4 py-3 text-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgb(var(--surface-1))",
|
||||||
|
borderColor: "rgb(var(--border-default))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4"
|
||||||
|
style={{ color: "rgb(var(--text-secondary))" }}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm" style={{ color: "rgb(var(--text-secondary))" }}>
|
||||||
|
Sign in to chat with Jarvis
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="sticky bottom-0 border-t"
|
className="sticky bottom-0 border-t"
|
||||||
style={{
|
style={{
|
||||||
@@ -352,7 +377,7 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
|
|||||||
<div className="mx-auto max-w-4xl px-4 py-4 lg:px-8">
|
<div className="mx-auto max-w-4xl px-4 py-4 lg:px-8">
|
||||||
<ChatInput
|
<ChatInput
|
||||||
onSend={handleSendMessage}
|
onSend={handleSendMessage}
|
||||||
disabled={isChatLoading}
|
disabled={isChatLoading || !user}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
onStopStreaming={abortStream}
|
onStopStreaming={abortStream}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export function ChatOverlay(): React.JSX.Element {
|
|||||||
onClick={open}
|
onClick={open}
|
||||||
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full shadow-lg transition-all hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 lg:bottom-8 lg:right-8"
|
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full shadow-lg transition-all hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 lg:bottom-8 lg:right-8"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "rgb(var(--accent-primary))",
|
backgroundColor: "var(--accent-primary, #10b981)",
|
||||||
color: "rgb(var(--text-on-accent))",
|
color: "var(--text-on-accent, #ffffff)",
|
||||||
}}
|
}}
|
||||||
aria-label="Open chat"
|
aria-label="Open chat"
|
||||||
title="Open Jarvis chat (Cmd+Shift+J)"
|
title="Open Jarvis chat (Cmd+Shift+J)"
|
||||||
@@ -78,18 +78,18 @@ export function ChatOverlay(): React.JSX.Element {
|
|||||||
if (isMinimized) {
|
if (isMinimized) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed bottom-0 right-0 z-40 w-full sm:w-96"
|
className="fixed bottom-0 right-0 z-40 w-full shadow-2xl sm:w-96"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "rgb(var(--surface-0))",
|
backgroundColor: "var(--surface-0, #ffffff)",
|
||||||
borderColor: "rgb(var(--border-default))",
|
borderColor: "var(--border-default, #e5e7eb)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={expand}
|
onClick={expand}
|
||||||
className="flex w-full items-center justify-between border-t px-4 py-3 text-left transition-colors hover:bg-black/5 focus:outline-none focus:ring-2 focus:ring-inset"
|
className="flex w-full items-center justify-between border-t px-4 py-3 text-left transition-colors hover:bg-black/5 focus:outline-none focus:ring-2 focus:ring-inset"
|
||||||
style={{
|
style={{
|
||||||
borderColor: "rgb(var(--border-default))",
|
borderColor: "var(--border-default, #e5e7eb)",
|
||||||
backgroundColor: "rgb(var(--surface-0))",
|
backgroundColor: "var(--surface-0, #ffffff)",
|
||||||
}}
|
}}
|
||||||
aria-label="Expand chat"
|
aria-label="Expand chat"
|
||||||
>
|
>
|
||||||
@@ -135,10 +135,10 @@ export function ChatOverlay(): React.JSX.Element {
|
|||||||
|
|
||||||
{/* Chat Panel */}
|
{/* Chat Panel */}
|
||||||
<div
|
<div
|
||||||
className="fixed inset-y-0 right-0 z-40 flex w-full flex-col border-l sm:w-96 lg:inset-y-16"
|
className="fixed inset-y-0 right-0 z-40 flex w-full flex-col border-l shadow-2xl sm:w-96 lg:inset-y-16"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "rgb(var(--surface-0))",
|
backgroundColor: "var(--surface-0, #ffffff)",
|
||||||
borderColor: "rgb(var(--border-default))",
|
borderColor: "var(--border-default, #e5e7eb)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -4,12 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useRef } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
import {
|
import { streamChatMessage, type ChatMessage as ApiChatMessage } from "@/lib/api/chat";
|
||||||
sendChatMessage,
|
|
||||||
streamChatMessage,
|
|
||||||
streamGuestChat,
|
|
||||||
type ChatMessage as ApiChatMessage,
|
|
||||||
} from "@/lib/api/chat";
|
|
||||||
import { createConversation, updateConversation, getIdea, type Idea } from "@/lib/api/ideas";
|
import { createConversation, updateConversation, getIdea, type Idea } from "@/lib/api/ideas";
|
||||||
import { safeJsonParse, isMessageArray } from "@/lib/utils/safe-json";
|
import { safeJsonParse, isMessageArray } from "@/lib/utils/safe-json";
|
||||||
|
|
||||||
@@ -219,8 +214,6 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
abortControllerRef.current = controller;
|
abortControllerRef.current = controller;
|
||||||
|
|
||||||
let streamingSucceeded = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
let hasReceivedData = false;
|
let hasReceivedData = false;
|
||||||
@@ -248,7 +241,6 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
streamingSucceeded = true;
|
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
resolve();
|
resolve();
|
||||||
@@ -279,140 +271,26 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streaming failed - check if auth error, try guest mode
|
// Streaming failed — show error (no guest fallback, auth required)
|
||||||
const isAuthError =
|
console.warn("Streaming failed", {
|
||||||
err instanceof Error &&
|
error: err instanceof Error ? err : new Error(String(err)),
|
||||||
(err.message.includes("403") ||
|
});
|
||||||
err.message.includes("401") ||
|
|
||||||
err.message.includes("auth") ||
|
|
||||||
err.message.includes("Forbidden"));
|
|
||||||
|
|
||||||
if (isAuthError) {
|
setMessages((prev) => {
|
||||||
console.warn("Auth failed, trying guest chat mode");
|
const withoutPlaceholder = prev.filter((m) => m.id !== assistantMessageId);
|
||||||
|
messagesRef.current = withoutPlaceholder;
|
||||||
|
return withoutPlaceholder;
|
||||||
|
});
|
||||||
|
setIsStreaming(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
// Try guest chat streaming
|
const errorMsg = err instanceof Error ? err.message : "Chat unavailable";
|
||||||
try {
|
setError(`Chat error: ${errorMsg}`);
|
||||||
await new Promise<void>((guestResolve, guestReject) => {
|
return;
|
||||||
let hasReceivedData = false;
|
|
||||||
|
|
||||||
streamGuestChat(
|
|
||||||
request,
|
|
||||||
(chunk: string) => {
|
|
||||||
if (!hasReceivedData) {
|
|
||||||
hasReceivedData = true;
|
|
||||||
setIsLoading(false);
|
|
||||||
setIsStreaming(true);
|
|
||||||
setMessages((prev) => {
|
|
||||||
const updated = [...prev, { ...placeholderMessage }];
|
|
||||||
messagesRef.current = updated;
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessages((prev) => {
|
|
||||||
const updated = prev.map((msg) =>
|
|
||||||
msg.id === assistantMessageId ? { ...msg, content: msg.content + chunk } : msg
|
|
||||||
);
|
|
||||||
messagesRef.current = updated;
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
streamingSucceeded = true;
|
|
||||||
setIsStreaming(false);
|
|
||||||
guestResolve();
|
|
||||||
},
|
|
||||||
(guestErr: Error) => {
|
|
||||||
guestReject(guestErr);
|
|
||||||
},
|
|
||||||
controller.signal
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (guestErr: unknown) {
|
|
||||||
// Guest also failed
|
|
||||||
setMessages((prev) => {
|
|
||||||
const withoutPlaceholder = prev.filter((m) => m.id !== assistantMessageId);
|
|
||||||
messagesRef.current = withoutPlaceholder;
|
|
||||||
return withoutPlaceholder;
|
|
||||||
});
|
|
||||||
const errorMsg = guestErr instanceof Error ? guestErr.message : "Chat unavailable";
|
|
||||||
setError(`Unable to connect to chat: ${errorMsg}`);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Streaming failed — fall back to non-streaming
|
|
||||||
console.warn("Streaming failed, falling back to non-streaming", {
|
|
||||||
error: err instanceof Error ? err : new Error(String(err)),
|
|
||||||
});
|
|
||||||
|
|
||||||
setMessages((prev) => {
|
|
||||||
const withoutPlaceholder = prev.filter((m) => m.id !== assistantMessageId);
|
|
||||||
messagesRef.current = withoutPlaceholder;
|
|
||||||
return withoutPlaceholder;
|
|
||||||
});
|
|
||||||
setIsStreaming(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await sendChatMessage(request);
|
|
||||||
|
|
||||||
const assistantMessage: Message = {
|
|
||||||
id: `assistant-${Date.now().toString()}`,
|
|
||||||
role: "assistant",
|
|
||||||
content: response.message.content,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
model: response.model,
|
|
||||||
promptTokens: response.promptEvalCount ?? 0,
|
|
||||||
completionTokens: response.evalCount ?? 0,
|
|
||||||
totalTokens: (response.promptEvalCount ?? 0) + (response.evalCount ?? 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
setMessages((prev) => {
|
|
||||||
const updated = [...prev, assistantMessage];
|
|
||||||
messagesRef.current = updated;
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
|
|
||||||
streamingSucceeded = true;
|
|
||||||
} catch (fallbackErr: unknown) {
|
|
||||||
const errorMsg =
|
|
||||||
fallbackErr instanceof Error ? fallbackErr.message : "Failed to send message";
|
|
||||||
setError("Unable to send message. Please try again.");
|
|
||||||
onError?.(fallbackErr instanceof Error ? fallbackErr : new Error(errorMsg));
|
|
||||||
console.error("Failed to send chat message", {
|
|
||||||
error: fallbackErr,
|
|
||||||
errorType: "LLM_ERROR",
|
|
||||||
conversationId: conversationIdRef.current,
|
|
||||||
messageLength: content.length,
|
|
||||||
messagePreview: content.substring(0, 50),
|
|
||||||
model,
|
|
||||||
messageCount: messagesRef.current.length,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorMessage: Message = {
|
|
||||||
id: `error-${String(Date.now())}`,
|
|
||||||
role: "assistant",
|
|
||||||
content: "Something went wrong. Please try again.",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
setMessages((prev) => {
|
|
||||||
const updated = [...prev, errorMessage];
|
|
||||||
messagesRef.current = updated;
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
if (!streamingSucceeded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalMessages = messagesRef.current;
|
const finalMessages = messagesRef.current;
|
||||||
|
|
||||||
const isFirstMessage =
|
const isFirstMessage =
|
||||||
|
|||||||
Reference in New Issue
Block a user