mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-05 22:54:23 +01:00
feat: implement continue chat (not resume if connected)
This commit is contained in:
@@ -28,8 +28,10 @@ export const useConfig = () => {
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: configQueryConfig.queryKey });
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: configQueryConfig.queryKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertCircleIcon, LoaderIcon, SendIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type FC, useId, useRef, useState } from "react";
|
||||
@@ -16,6 +16,7 @@ export const NewChat: FC<{
|
||||
}> = ({ projectId, onSuccess }) => {
|
||||
const router = useRouter();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const startNewChat = useMutation({
|
||||
mutationFn: async (options: { message: string }) => {
|
||||
@@ -30,13 +31,17 @@ export const NewChat: FC<{
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["aliveTasks"] });
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
onSuccess: async (response) => {
|
||||
setMessage("");
|
||||
onSuccess?.();
|
||||
await queryClient.invalidateQueries({ queryKey: ["aliveTasks"] });
|
||||
|
||||
router.push(
|
||||
`/projects/${projectId}/sessions/${response.nextSessionId}#message-${response.userMessageId}`,
|
||||
`/projects/${projectId}/sessions/${response.sessionId}#message-${response.userMessageId}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ArrowLeftIcon, LoaderIcon, XIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, LoaderIcon, PauseIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { honoClient } from "../../../../../../lib/api/client";
|
||||
import { firstCommandToTitle } from "../../../services/firstCommandToTitle";
|
||||
import { useIsResummingTask } from "../hooks/useIsResummingTask";
|
||||
import { useAliveTask } from "../hooks/useAliveTask";
|
||||
import { useSession } from "../hooks/useSession";
|
||||
import { ConversationList } from "./conversationList/ConversationList";
|
||||
import { ResumeChat } from "./resumeChat/ResumeChat";
|
||||
@@ -37,15 +37,16 @@ export const SessionPageContent: FC<{
|
||||
},
|
||||
});
|
||||
|
||||
const { isResummingTask } = useIsResummingTask(sessionId);
|
||||
const { isRunningTask, isPausedTask } = useAliveTask(sessionId);
|
||||
|
||||
const [previouConversationLength, setPreviouConversationLength] = useState(0);
|
||||
const [previousConversationLength, setPreviousConversationLength] =
|
||||
useState(0);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 自動スクロール処理
|
||||
useEffect(() => {
|
||||
if (isResummingTask && conversations.length !== previouConversationLength) {
|
||||
setPreviouConversationLength(conversations.length);
|
||||
if (isRunningTask && conversations.length !== previousConversationLength) {
|
||||
setPreviousConversationLength(conversations.length);
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
@@ -54,7 +55,7 @@ export const SessionPageContent: FC<{
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [conversations, isResummingTask, previouConversationLength]);
|
||||
}, [conversations, isRunningTask, previousConversationLength]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
@@ -85,12 +86,33 @@ export const SessionPageContent: FC<{
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{isResummingTask && (
|
||||
{isRunningTask && (
|
||||
<div className="flex items-center gap-2 p-3 bg-primary/10 border border-primary/20 rounded-lg">
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">
|
||||
Conversation is being resumed...
|
||||
Conversation is in progress...
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
abortTask.mutate(sessionId);
|
||||
}}
|
||||
>
|
||||
<XIcon className="w-4 h-4" />
|
||||
Abort
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPausedTask && (
|
||||
<div className="flex items-center gap-2 p-3 bg-primary/10 border border-primary/20 rounded-lg">
|
||||
<PauseIcon className="w-4 h-4" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">
|
||||
Conversation is paused...
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -118,7 +140,11 @@ export const SessionPageContent: FC<{
|
||||
getToolResult={getToolResult}
|
||||
/>
|
||||
|
||||
<ResumeChat projectId={projectId} sessionId={sessionId} />
|
||||
<ResumeChat
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
isPausedTask={isPausedTask}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
export const ResumeChat: FC<{
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
}> = ({ projectId, sessionId }) => {
|
||||
isPausedTask: boolean;
|
||||
}> = ({ projectId, sessionId, isPausedTask }) => {
|
||||
const router = useRouter();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -44,14 +45,18 @@ export const ResumeChat: FC<{
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["aliveTasks"] });
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
onSuccess: async (response) => {
|
||||
setMessage("");
|
||||
queryClient.invalidateQueries({ queryKey: ["runningTasks"] });
|
||||
router.push(
|
||||
`/projects/${projectId}/sessions/${response.nextSessionId}#message-${response.userMessageId}`,
|
||||
);
|
||||
await queryClient.invalidateQueries({ queryKey: ["aliveTasks"] });
|
||||
if (sessionId !== response.sessionId) {
|
||||
router.push(
|
||||
`/projects/${projectId}/sessions/${response.sessionId}#message-${response.userMessageId}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -143,12 +148,17 @@ export const ResumeChat: FC<{
|
||||
{resumeChat.isPending ? (
|
||||
<>
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
Starting...
|
||||
Starting... This may take a while.
|
||||
</>
|
||||
) : isPausedTask ? (
|
||||
<>
|
||||
<SendIcon className="w-4 h-4" />
|
||||
Send
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon className="w-4 h-4" />
|
||||
Resume Chat
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { honoClient } from "../../../../../../lib/api/client";
|
||||
|
||||
export const useAliveTask = (sessionId: string) => {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["aliveTasks"],
|
||||
queryFn: async () => {
|
||||
const response = await honoClient.api.tasks.alive.$get({});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
|
||||
const taskInfo = useMemo(() => {
|
||||
const aliveTask = data?.aliveTasks.find(
|
||||
(task) => task.sessionId === sessionId,
|
||||
);
|
||||
return {
|
||||
aliveTask,
|
||||
isRunningTask: aliveTask?.status === "running",
|
||||
isPausedTask: aliveTask?.status === "paused",
|
||||
} as const;
|
||||
}, [data, sessionId]);
|
||||
|
||||
return taskInfo;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { honoClient } from "../../../../../../lib/api/client";
|
||||
|
||||
export const useIsResummingTask = (sessionId: string) => {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["runningTasks"],
|
||||
queryFn: async () => {
|
||||
const response = await honoClient.api.tasks.running.$get({});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
// Only poll when there might be running tasks
|
||||
refetchInterval: (query) => {
|
||||
const hasRunningTasks = (query.state.data?.runningTasks?.length ?? 0) > 0;
|
||||
return hasRunningTasks ? 2000 : false; // Poll every 2s when there are tasks, stop when none
|
||||
},
|
||||
// Keep data fresh for 30 seconds
|
||||
staleTime: 30 * 1000,
|
||||
// Keep in cache for 5 minutes
|
||||
gcTime: 5 * 60 * 1000,
|
||||
// Refetch when window regains focus
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
|
||||
const taskInfo = useMemo(() => {
|
||||
const runningTask = data?.runningTasks.find(
|
||||
(task) => task.nextSessionId === sessionId,
|
||||
);
|
||||
return {
|
||||
isResummingTask: Boolean(runningTask),
|
||||
task: runningTask,
|
||||
hasRunningTasks: (data?.runningTasks.length ?? 0) > 0,
|
||||
};
|
||||
}, [data, sessionId]);
|
||||
|
||||
return {
|
||||
...taskInfo,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user