Compare commits
3 Commits
main
...
hoyyeva/re
| Author | SHA1 | Date |
|---|---|---|
|
|
42d6a3f075 | |
|
|
ed553f51f7 | |
|
|
7d6f0c621f |
|
|
@ -305,6 +305,9 @@ func main() {
|
||||||
go func() {
|
go func() {
|
||||||
<-signals
|
<-signals
|
||||||
slog.Info("received SIGINT or SIGTERM signal, shutting down")
|
slog.Info("received SIGINT or SIGTERM signal, shutting down")
|
||||||
|
if err := st.ClearAllDrafts(); err != nil {
|
||||||
|
slog.Warn("failed to clear drafts on shutdown", "error", err)
|
||||||
|
}
|
||||||
quit()
|
quit()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,11 @@ func osRun(_ func(), hasCompletedFirstRun, startHidden bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func quit() {
|
func quit() {
|
||||||
|
if wv.Store != nil {
|
||||||
|
if err := wv.Store.ClearAllDrafts(); err != nil {
|
||||||
|
slog.Warn("failed to clear drafts on quit", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
C.quit()
|
C.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,11 @@ func (*appCallbacks) UIRunning() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appCallbacks) Quit() {
|
func (app *appCallbacks) Quit() {
|
||||||
|
if wv.Store != nil {
|
||||||
|
if err := wv.Store.ClearAllDrafts(); err != nil {
|
||||||
|
slog.Warn("failed to clear drafts on quit", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
app.t.Quit()
|
app.t.Quit()
|
||||||
wv.Terminate()
|
wv.Terminate()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
|
|
||||||
// currentSchemaVersion defines the current database schema version.
|
// currentSchemaVersion defines the current database schema version.
|
||||||
// Increment this when making schema changes that require migrations.
|
// Increment this when making schema changes that require migrations.
|
||||||
const currentSchemaVersion = 12
|
const currentSchemaVersion = 13
|
||||||
|
|
||||||
// database wraps the SQLite connection.
|
// database wraps the SQLite connection.
|
||||||
// SQLite handles its own locking for concurrent access:
|
// SQLite handles its own locking for concurrent access:
|
||||||
|
|
@ -95,7 +95,8 @@ func (db *database) init() error {
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
title TEXT NOT NULL DEFAULT '',
|
title TEXT NOT NULL DEFAULT '',
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
browser_state TEXT
|
browser_state TEXT,
|
||||||
|
draft TEXT NOT NULL DEFAULT ''
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
|
@ -244,6 +245,12 @@ func (db *database) migrate() error {
|
||||||
return fmt.Errorf("migrate v11 to v12: %w", err)
|
return fmt.Errorf("migrate v11 to v12: %w", err)
|
||||||
}
|
}
|
||||||
version = 12
|
version = 12
|
||||||
|
case 12:
|
||||||
|
// add draft column to chats table
|
||||||
|
if err := db.migrateV12ToV13(); err != nil {
|
||||||
|
return fmt.Errorf("migrate v12 to v13: %w", err)
|
||||||
|
}
|
||||||
|
version = 13
|
||||||
default:
|
default:
|
||||||
// If we have a version we don't recognize, just set it to current
|
// If we have a version we don't recognize, just set it to current
|
||||||
// This might happen during development
|
// This might happen during development
|
||||||
|
|
@ -452,6 +459,21 @@ func (db *database) migrateV11ToV12() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrateV12ToV13 adds the draft column to the chats table
|
||||||
|
func (db *database) migrateV12ToV13() error {
|
||||||
|
_, err := db.conn.Exec(`ALTER TABLE chats ADD COLUMN draft TEXT NOT NULL DEFAULT ''`)
|
||||||
|
if err != nil && !duplicateColumnError(err) {
|
||||||
|
return fmt.Errorf("add draft column: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.conn.Exec(`UPDATE settings SET schema_version = 13`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update schema version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// cleanupOrphanedData removes orphaned records that may exist due to the foreign key bug
|
// cleanupOrphanedData removes orphaned records that may exist due to the foreign key bug
|
||||||
func (db *database) cleanupOrphanedData() error {
|
func (db *database) cleanupOrphanedData() error {
|
||||||
_, err := db.conn.Exec(`
|
_, err := db.conn.Exec(`
|
||||||
|
|
@ -570,7 +592,7 @@ func (db *database) getAllChats() ([]Chat, error) {
|
||||||
|
|
||||||
func (db *database) getChatWithOptions(id string, loadAttachmentData bool) (*Chat, error) {
|
func (db *database) getChatWithOptions(id string, loadAttachmentData bool) (*Chat, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, title, created_at, browser_state
|
SELECT id, title, created_at, browser_state, draft
|
||||||
FROM chats
|
FROM chats
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`
|
`
|
||||||
|
|
@ -578,12 +600,14 @@ func (db *database) getChatWithOptions(id string, loadAttachmentData bool) (*Cha
|
||||||
var chat Chat
|
var chat Chat
|
||||||
var createdAt time.Time
|
var createdAt time.Time
|
||||||
var browserState sql.NullString
|
var browserState sql.NullString
|
||||||
|
var draft sql.NullString
|
||||||
|
|
||||||
err := db.conn.QueryRow(query, id).Scan(
|
err := db.conn.QueryRow(query, id).Scan(
|
||||||
&chat.ID,
|
&chat.ID,
|
||||||
&chat.Title,
|
&chat.Title,
|
||||||
&createdAt,
|
&createdAt,
|
||||||
&browserState,
|
&browserState,
|
||||||
|
&draft,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
|
@ -599,6 +623,9 @@ func (db *database) getChatWithOptions(id string, loadAttachmentData bool) (*Cha
|
||||||
chat.BrowserState = raw
|
chat.BrowserState = raw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if draft.Valid {
|
||||||
|
chat.Draft = draft.String
|
||||||
|
}
|
||||||
|
|
||||||
messages, err := db.getMessages(id, loadAttachmentData)
|
messages, err := db.getMessages(id, loadAttachmentData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -622,11 +649,12 @@ func (db *database) saveChat(chat Chat) error {
|
||||||
// UPSERT would overwrite browser_state with NULL, breaking revisit rendering that relies
|
// UPSERT would overwrite browser_state with NULL, breaking revisit rendering that relies
|
||||||
// on the last persisted full tool state.
|
// on the last persisted full tool state.
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO chats (id, title, created_at, browser_state)
|
INSERT INTO chats (id, title, created_at, browser_state, draft)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
title = excluded.title,
|
title = excluded.title,
|
||||||
browser_state = COALESCE(excluded.browser_state, chats.browser_state)
|
browser_state = COALESCE(excluded.browser_state, chats.browser_state),
|
||||||
|
draft = excluded.draft
|
||||||
`
|
`
|
||||||
|
|
||||||
var browserState sql.NullString
|
var browserState sql.NullString
|
||||||
|
|
@ -639,6 +667,7 @@ func (db *database) saveChat(chat Chat) error {
|
||||||
chat.Title,
|
chat.Title,
|
||||||
chat.CreatedAt,
|
chat.CreatedAt,
|
||||||
browserState,
|
browserState,
|
||||||
|
chat.Draft,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("save chat: %w", err)
|
return fmt.Errorf("save chat: %w", err)
|
||||||
|
|
@ -669,6 +698,23 @@ func (db *database) saveChat(chat Chat) error {
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateChatDraft updates only the draft for a chat
|
||||||
|
func (db *database) updateChatDraft(chatID string, draft string) error {
|
||||||
|
_, err := db.conn.Exec(`UPDATE chats SET draft = ? WHERE id = ?`, draft, chatID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update chat draft: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *database) clearAllDrafts() error {
|
||||||
|
_, err := db.conn.Exec(`UPDATE chats SET draft = ''`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("clear all drafts: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// updateChatBrowserState updates only the browser_state for a chat
|
// updateChatBrowserState updates only the browser_state for a chat
|
||||||
func (db *database) updateChatBrowserState(chatID string, state json.RawMessage) error {
|
func (db *database) updateChatBrowserState(chatID string, state json.RawMessage) error {
|
||||||
_, err := db.conn.Exec(`UPDATE chats SET browser_state = ? WHERE id = ?`, string(state), chatID)
|
_, err := db.conn.Exec(`UPDATE chats SET browser_state = ? WHERE id = ?`, string(state), chatID)
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ type Chat struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
BrowserState json.RawMessage `json:"browser_state,omitempty" ts_type:"BrowserStateData"`
|
BrowserState json.RawMessage `json:"browser_state,omitempty" ts_type:"BrowserStateData"`
|
||||||
|
Draft string `json:"draft,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChat creates a new Chat with the ID, with CreatedAt timestamp initialized
|
// NewChat creates a new Chat with the ID, with CreatedAt timestamp initialized
|
||||||
|
|
@ -451,6 +452,22 @@ func (s *Store) AppendMessage(chatID string, message Message) error {
|
||||||
return s.db.appendMessage(chatID, message)
|
return s.db.appendMessage(chatID, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateChatDraft(chatID string, draft string) error {
|
||||||
|
if err := s.ensureDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.updateChatDraft(chatID, draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ClearAllDrafts() error {
|
||||||
|
if err := s.ensureDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.clearAllDrafts()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateChatBrowserState(chatID string, state json.RawMessage) error {
|
func (s *Store) UpdateChatBrowserState(chatID string, state json.RawMessage) error {
|
||||||
if err := s.ensureDB(); err != nil {
|
if err := s.ensureDB(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ export class Chat {
|
||||||
title: string;
|
title: string;
|
||||||
created_at: Time;
|
created_at: Time;
|
||||||
browser_state?: BrowserStateData;
|
browser_state?: BrowserStateData;
|
||||||
|
draft?: string;
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
|
@ -167,6 +168,7 @@ export class Chat {
|
||||||
this.title = source["title"];
|
this.title = source["title"];
|
||||||
this.created_at = this.convertValues(source["created_at"], Time);
|
this.created_at = this.convertValues(source["created_at"], Time);
|
||||||
this.browser_state = source["browser_state"];
|
this.browser_state = source["browser_state"];
|
||||||
|
this.draft = source["draft"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,20 @@ export async function renameChat(chatId: string, title: string): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateChatDraft(chatId: string, draft: string): Promise<void> {
|
||||||
|
const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}/draft`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ draft }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(error || "Failed to update draft");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteChat(chatId: string): Promise<void> {
|
export async function deleteChat(chatId: string): Promise<void> {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}`, {
|
const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ export default function Chat({ chatId }: { chatId: string }) {
|
||||||
onSubmit={handleChatFormSubmit}
|
onSubmit={handleChatFormSubmit}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
initialDraft={chatQuery?.data?.chat?.draft ?? ""}
|
||||||
editingMessage={editingMessage}
|
editingMessage={editingMessage}
|
||||||
onCancelEdit={handleCancelEdit}
|
onCancelEdit={handleCancelEdit}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { ErrorMessage } from "./ErrorMessage";
|
||||||
import { processFiles } from "@/utils/fileValidation";
|
import { processFiles } from "@/utils/fileValidation";
|
||||||
import type { ImageData } from "@/types/webview";
|
import type { ImageData } from "@/types/webview";
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { useDraftMessage } from "@/hooks/useDraftMessage";
|
||||||
|
|
||||||
export type ThinkingLevel = "low" | "medium" | "high";
|
export type ThinkingLevel = "low" | "medium" | "high";
|
||||||
|
|
||||||
|
|
@ -62,6 +63,7 @@ interface ChatFormProps {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
isDownloadingModel?: boolean;
|
isDownloadingModel?: boolean;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
initialDraft?: string;
|
||||||
// Editing props - when provided, ChatForm enters edit mode
|
// Editing props - when provided, ChatForm enters edit mode
|
||||||
editingMessage?: {
|
editingMessage?: {
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -84,6 +86,7 @@ function ChatForm({
|
||||||
chatId = "new",
|
chatId = "new",
|
||||||
isDownloadingModel = false,
|
isDownloadingModel = false,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
|
initialDraft,
|
||||||
editingMessage,
|
editingMessage,
|
||||||
onCancelEdit,
|
onCancelEdit,
|
||||||
onFilesReceived,
|
onFilesReceived,
|
||||||
|
|
@ -118,6 +121,8 @@ function ChatForm({
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { saveDraft, clearDraft } = useDraftMessage(chatId);
|
||||||
|
|
||||||
const handleThinkingLevelDropdownToggle = (isOpen: boolean) => {
|
const handleThinkingLevelDropdownToggle = (isOpen: boolean) => {
|
||||||
if (
|
if (
|
||||||
isOpen &&
|
isOpen &&
|
||||||
|
|
@ -308,10 +313,39 @@ function ChatForm({
|
||||||
}
|
}
|
||||||
}, [editingMessage]);
|
}, [editingMessage]);
|
||||||
|
|
||||||
// Clear composition and reset textarea height when chatId changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetChatForm();
|
if (editingMessage) {
|
||||||
}, [chatId]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialDraft && initialDraft.trim()) {
|
||||||
|
setMessage({
|
||||||
|
content: initialDraft,
|
||||||
|
attachments: [],
|
||||||
|
fileErrors: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adjust textarea height after loading draft
|
||||||
|
setTimeout(() => {
|
||||||
|
if (textareaRef.current && initialDraft) {
|
||||||
|
textareaRef.current.style.height = "auto";
|
||||||
|
textareaRef.current.style.height =
|
||||||
|
Math.min(textareaRef.current.scrollHeight, 24 * 8) + "px";
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
resetChatForm();
|
||||||
|
}
|
||||||
|
}, [chatId, initialDraft, editingMessage]);
|
||||||
|
|
||||||
|
// Save draft only when navigating away or on blur
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (!editingMessage && message.content.trim()) {
|
||||||
|
saveDraft(message.content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [message.content, editingMessage, saveDraft]);
|
||||||
|
|
||||||
// Auto-focus textarea when autoFocus is true or when streaming completes (but not when editing)
|
// Auto-focus textarea when autoFocus is true or when streaming completes (but not when editing)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -511,12 +545,13 @@ function ChatForm({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear composition after successful submission
|
// Clear composition and draft after successful submission
|
||||||
setMessage({
|
setMessage({
|
||||||
content: "",
|
content: "",
|
||||||
attachments: [],
|
attachments: [],
|
||||||
fileErrors: [],
|
fileErrors: [],
|
||||||
});
|
});
|
||||||
|
clearDraft();
|
||||||
|
|
||||||
// Reset textarea height and refocus after submit
|
// Reset textarea height and refocus after submit
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -621,6 +656,13 @@ function ChatForm({
|
||||||
e.target.style.height = Math.min(e.target.scrollHeight, 24 * 8) + "px";
|
e.target.style.height = Math.min(e.target.scrollHeight, 24 * 8) + "px";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save draft when textarea loses focus
|
||||||
|
const handleTextareaBlur = () => {
|
||||||
|
if (!editingMessage && message.content.trim()) {
|
||||||
|
saveDraft(message.content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilesUpload = async () => {
|
const handleFilesUpload = async () => {
|
||||||
try {
|
try {
|
||||||
setFileUploadError(null);
|
setFileUploadError(null);
|
||||||
|
|
@ -832,6 +874,7 @@ function ChatForm({
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={message.content}
|
value={message.content}
|
||||||
onChange={handleTextareaChange}
|
onChange={handleTextareaChange}
|
||||||
|
onBlur={handleTextareaBlur}
|
||||||
placeholder="Send a message"
|
placeholder="Send a message"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={`allow-context-menu w-full overflow-y-auto text-neutral-700 outline-none resize-none border-none bg-transparent dark:text-white placeholder:text-neutral-400 dark:placeholder:text-neutral-500 min-h-[24px] leading-6 transition-opacity duration-300 ${
|
className={`allow-context-menu w-full overflow-y-auto text-neutral-700 outline-none resize-none border-none bg-transparent dark:text-white placeholder:text-neutral-400 dark:placeholder:text-neutral-500 min-h-[24px] leading-6 transition-opacity duration-300 ${
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import { Settings as SettingsType } from "@/gotypes";
|
import { Settings as SettingsType } from "@/gotypes";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
import { useUser } from "@/hooks/useUser";
|
import { useUser } from "@/hooks/useUser";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { getSettings, updateSettings } from "@/api";
|
import { getSettings, updateSettings } from "@/api";
|
||||||
|
|
@ -52,7 +51,6 @@ export default function Settings() {
|
||||||
const [isAwaitingConnection, setIsAwaitingConnection] = useState(false);
|
const [isAwaitingConnection, setIsAwaitingConnection] = useState(false);
|
||||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||||
const [pollingInterval, setPollingInterval] = useState<number | null>(null);
|
const [pollingInterval, setPollingInterval] = useState<number | null>(null);
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: settingsData,
|
data: settingsData,
|
||||||
|
|
@ -216,7 +214,7 @@ export default function Settings() {
|
||||||
>
|
>
|
||||||
{isWindows && (
|
{isWindows && (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate({ to: "/" })}
|
onClick={() => window.history.back()}
|
||||||
className="hover:bg-neutral-100 mr-3 dark:hover:bg-neutral-800 rounded-full p-1.5"
|
className="hover:bg-neutral-100 mr-3 dark:hover:bg-neutral-800 rounded-full p-1.5"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-5 h-5 dark:text-white" />
|
<ArrowLeftIcon className="w-5 h-5 dark:text-white" />
|
||||||
|
|
@ -226,7 +224,7 @@ export default function Settings() {
|
||||||
</h1>
|
</h1>
|
||||||
{!isWindows && (
|
{!isWindows && (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate({ to: "/" })}
|
onClick={() => window.history.back()}
|
||||||
className="p-1 hover:bg-neutral-100 mr-3 dark:hover:bg-neutral-800 rounded-full"
|
className="p-1 hover:bg-neutral-100 mr-3 dark:hover:bg-neutral-800 rounded-full"
|
||||||
>
|
>
|
||||||
<XMarkIcon className="w-6 h-6 dark:text-white" />
|
<XMarkIcon className="w-6 h-6 dark:text-white" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { updateChatDraft } from "@/api";
|
||||||
|
|
||||||
|
export function useDraftMessage(chatId: string) {
|
||||||
|
const saveDraft = useCallback(async (content: string) => {
|
||||||
|
try {
|
||||||
|
if (chatId === "new") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateChatDraft(chatId, content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving draft message:", error);
|
||||||
|
}
|
||||||
|
}, [chatId]);
|
||||||
|
|
||||||
|
const clearDraft = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
if (chatId === "new") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateChatDraft(chatId, "");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error clearing draft message:", error);
|
||||||
|
}
|
||||||
|
}, [chatId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveDraft,
|
||||||
|
clearDraft,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
23
app/ui/ui.go
23
app/ui/ui.go
|
|
@ -253,6 +253,7 @@ func (s *Server) Handler() http.Handler {
|
||||||
mux.Handle("DELETE /api/v1/chat/{id}", handle(s.deleteChat))
|
mux.Handle("DELETE /api/v1/chat/{id}", handle(s.deleteChat))
|
||||||
mux.Handle("POST /api/v1/create-chat", handle(s.createChat))
|
mux.Handle("POST /api/v1/create-chat", handle(s.createChat))
|
||||||
mux.Handle("PUT /api/v1/chat/{id}/rename", handle(s.renameChat))
|
mux.Handle("PUT /api/v1/chat/{id}/rename", handle(s.renameChat))
|
||||||
|
mux.Handle("PUT /api/v1/chat/{id}/draft", handle(s.updateDraft))
|
||||||
|
|
||||||
mux.Handle("GET /api/v1/inference-compute", handle(s.getInferenceCompute))
|
mux.Handle("GET /api/v1/inference-compute", handle(s.getInferenceCompute))
|
||||||
mux.Handle("POST /api/v1/model/upstream", handle(s.modelUpstream))
|
mux.Handle("POST /api/v1/model/upstream", handle(s.modelUpstream))
|
||||||
|
|
@ -1276,6 +1277,28 @@ func (s *Server) renameChat(w http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) updateDraft(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
cid := r.PathValue("id")
|
||||||
|
if cid == "" {
|
||||||
|
return fmt.Errorf("chat ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Draft string `json:"draft"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
return fmt.Errorf("invalid request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Store.UpdateChatDraft(cid, req.Draft); err != nil {
|
||||||
|
return fmt.Errorf("failed to update draft: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) deleteChat(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) deleteChat(w http.ResponseWriter, r *http.Request) error {
|
||||||
cid := r.PathValue("id")
|
cid := r.PathValue("id")
|
||||||
if cid == "" {
|
if cid == "" {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue