feat: inline file/screenshot attachment in triage chat
Some checks failed
Test / frontend-typecheck (push) Waiting to run
Test / frontend-tests (push) Waiting to run
Auto Tag / auto-tag (push) Successful in 4s
Test / rust-fmt-check (push) Successful in 1m11s
Release / build-macos-arm64 (push) Successful in 3m43s
Test / rust-clippy (push) Successful in 7m10s
Release / build-linux-amd64 (push) Has been cancelled
Release / build-windows-amd64 (push) Has been cancelled
Release / build-linux-arm64 (push) Has been cancelled
Test / rust-tests (push) Has been cancelled
Some checks failed
Test / frontend-typecheck (push) Waiting to run
Test / frontend-tests (push) Waiting to run
Auto Tag / auto-tag (push) Successful in 4s
Test / rust-fmt-check (push) Successful in 1m11s
Release / build-macos-arm64 (push) Successful in 3m43s
Test / rust-clippy (push) Successful in 7m10s
Release / build-linux-amd64 (push) Has been cancelled
Release / build-windows-amd64 (push) Has been cancelled
Release / build-linux-arm64 (push) Has been cancelled
Test / rust-tests (push) Has been cancelled
- NewIssue navigates directly to /triage — log upload is never a blocker - ChatWindow: paperclip button opens Tauri file dialog; pending files shown as removable chips above the input; send enabled with files and no text - Triage: uploads selected files via uploadLogFileCmd, reads text content (capped at 8KB), appends file contents to AI message for context while showing only filenames in the chat bubble - Images/binary files are referenced by name with a prompt for the user to describe them
This commit is contained in:
parent
ebf60d17c6
commit
5537b0b042
@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Send, Bot, User } from "lucide-react";
|
||||
import { Send, Bot, User, Paperclip, X } from "lucide-react";
|
||||
import type { TriageMessage } from "@/lib/tauriCommands";
|
||||
|
||||
interface ChatWindowProps {
|
||||
@ -7,9 +7,12 @@ interface ChatWindowProps {
|
||||
onSend: (message: string) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
placeholder?: string;
|
||||
pendingFiles?: { name: string }[];
|
||||
onAttach?: () => void;
|
||||
onRemoveFile?: (index: number) => void;
|
||||
}
|
||||
|
||||
export function ChatWindow({ messages, onSend, isLoading, placeholder }: ChatWindowProps) {
|
||||
export function ChatWindow({ messages, onSend, isLoading, placeholder, pendingFiles, onAttach, onRemoveFile }: ChatWindowProps) {
|
||||
const [input, setInput] = useState("");
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -76,20 +79,48 @@ export function ChatWindow({ messages, onSend, isLoading, placeholder }: ChatWin
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
<div className="border-t p-4">
|
||||
<div className="border-t p-4 space-y-2">
|
||||
{pendingFiles && pendingFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{pendingFiles.map((f, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-xs text-secondary-foreground"
|
||||
>
|
||||
<Paperclip className="w-3 h-3" />
|
||||
{f.name}
|
||||
{onRemoveFile && (
|
||||
<button onClick={() => onRemoveFile(i)} className="ml-1 hover:text-destructive">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{onAttach && (
|
||||
<button
|
||||
onClick={onAttach}
|
||||
disabled={isLoading}
|
||||
title="Attach log file or screenshot"
|
||||
className="px-3 py-2 rounded-md border border-input bg-background hover:bg-accent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Paperclip className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
)}
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder ?? "Type your response... (Enter to send, Shift+Enter for new line)"}
|
||||
rows={2}
|
||||
className="flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || isLoading}
|
||||
disabled={(!input.trim() && (!pendingFiles || pendingFiles.length === 0)) || isLoading}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
|
||||
@ -53,7 +53,7 @@ export default function NewIssue() {
|
||||
try {
|
||||
const issue = await createIssueCmd({ title: title.trim(), domain: selectedDomain, severity });
|
||||
startSession(issue);
|
||||
navigate(`/issue/${issue.id}/logs`);
|
||||
navigate(`/issue/${issue.id}/triage`);
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
setIsSubmitting(false);
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { CheckCircle, ChevronRight } from "lucide-react";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { ChatWindow } from "@/components/ChatWindow";
|
||||
import { TriageProgress } from "@/components/TriageProgress";
|
||||
import { useSessionStore } from "@/stores/sessionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { chatMessageCmd, getIssueCmd } from "@/lib/tauriCommands";
|
||||
import { chatMessageCmd, getIssueCmd, uploadLogFileCmd } from "@/lib/tauriCommands";
|
||||
import type { TriageMessage } from "@/lib/tauriCommands";
|
||||
|
||||
type PendingFile = { name: string; content: string | null };
|
||||
|
||||
export default function Triage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
||||
const initialized = useRef(false);
|
||||
|
||||
const { currentIssue, messages, currentWhyLevel, startSession, addMessage, setWhyLevel } =
|
||||
@ -42,6 +47,35 @@ export default function Triage() {
|
||||
.catch((e) => setError(String(e)));
|
||||
}, [id]);
|
||||
|
||||
const handleAttach = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const selected = await open({
|
||||
multiple: true,
|
||||
filters: [
|
||||
{ name: "Log & Text Files", extensions: ["log", "txt", "json", "xml", "yaml", "yml", "csv", "out", "err"] },
|
||||
{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] },
|
||||
{ name: "All Files", extensions: ["*"] },
|
||||
],
|
||||
});
|
||||
if (!selected) return;
|
||||
const paths = Array.isArray(selected) ? selected : [selected];
|
||||
for (const filePath of paths) {
|
||||
const logFile = await uploadLogFileCmd(id, filePath);
|
||||
let content: string | null = null;
|
||||
try {
|
||||
const raw = await readTextFile(filePath);
|
||||
content = raw.slice(0, 8000); // cap at 8 KB to keep context manageable
|
||||
} catch {
|
||||
// Binary file (image) — include filename only as context
|
||||
}
|
||||
setPendingFiles((prev) => [...prev, { name: logFile.file_name, content }]);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(`Attachment failed: ${String(e)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async (message: string) => {
|
||||
if (!id || !currentIssue) return;
|
||||
const provider = getActiveProvider();
|
||||
@ -53,18 +87,33 @@ export default function Triage() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Build AI context: user text + file contents; show only text + filenames in chat UI
|
||||
const fileContext = pendingFiles
|
||||
.map((f) =>
|
||||
f.content
|
||||
? `\n\n--- Attached: ${f.name} ---\n${f.content}`
|
||||
: `\n\n[Image attached: ${f.name} — describe what you see if relevant]`
|
||||
)
|
||||
.join("");
|
||||
const aiMessage = pendingFiles.length > 0 ? `${message}${fileContext}` : message;
|
||||
const displayContent =
|
||||
pendingFiles.length > 0
|
||||
? `${message}${message ? "\n" : ""}📎 ${pendingFiles.map((f) => f.name).join(", ")}`
|
||||
: message;
|
||||
|
||||
const userMsg: TriageMessage = {
|
||||
id: `user-${Date.now()}`,
|
||||
issue_id: id,
|
||||
role: "user",
|
||||
content: message,
|
||||
content: displayContent,
|
||||
why_level: currentWhyLevel,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
addMessage(userMsg);
|
||||
setPendingFiles([]);
|
||||
|
||||
try {
|
||||
const response = await chatMessageCmd(id, message, provider);
|
||||
const response = await chatMessageCmd(id, aiMessage, provider);
|
||||
const assistantMsg: TriageMessage = {
|
||||
id: `asst-${Date.now()}`,
|
||||
issue_id: id,
|
||||
@ -126,6 +175,9 @@ export default function Triage() {
|
||||
onSend={handleSend}
|
||||
isLoading={isLoading}
|
||||
placeholder="Describe the problem or answer the AI's question..."
|
||||
pendingFiles={pendingFiles}
|
||||
onAttach={handleAttach}
|
||||
onRemoveFile={(i) => setPendingFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user