Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import "./App.css";
import { Button } from "./components/ui/button";
import {
Expand Down Expand Up @@ -86,13 +86,21 @@ function Home() {
</div>
)}
<div className="w-[60%] h-screen px-5">
{!!curPath && <PostEditor curPath={curPath} setCurPath={setCurPath} />}
{!!curPath && <
PostEditor
curPath={curPath}
setCurPath={setCurPath}
editorContent={editorContent}
setEditorContent={setEditorContent}
setEditorContentWrapper={setEditorContentWrapper}
/>}
</div>
<div className="flex-1 flex-grow relative px-4 bg-sidebar">
<ChatInterface
sessionId={sessionId}
editorTitle={editorTitle}
editorContent={editorContent}
setEditorContentWrapper={setEditorContentWrapper}
onClickNewChat={() => {
setSessionId(nanoid());
}}
Expand Down
77 changes: 72 additions & 5 deletions react/src/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ const ChatInterface = ({
onClickNewChat,
editorContent,
editorTitle,
setEditorContentWrapper,
}: {
sessionId: string;
editorTitle: string;
editorContent: string;
onClickNewChat: () => void;
setEditorContentWrapper: (value: string) => void;
}) => {
const [buttonClicked, setButtonClicked] = useState(true);
const [messages, setMessages] = useState<Message[]>([]);
const [prompt, setPrompt] = useState("");
const [disableStop, setDisableStop] = useState(false);
Expand Down Expand Up @@ -125,6 +128,7 @@ const ChatInterface = ({
console.log(event.data);
try {
const data = JSON.parse(event.data);
console.log("👇data", data);
if (data.type == "log") {
console.log(data);
}
Expand All @@ -139,6 +143,7 @@ const ChatInterface = ({
});
} else if (data.type == "done") {
setPending(false);
setButtonClicked(false);
} else if (data.type == "info") {
toast.info(data.info, {
closeButton: true,
Expand Down Expand Up @@ -236,6 +241,10 @@ const ChatInterface = ({
if (pending) {
return;
}
if (!buttonClicked) {
toast.error("Please approve/decline the previous AI suggestion first.");
return;
}
if (!model) {
toast.error(
"Please select a model! Go to Settings to set your API keys if you haven't done so."
Expand All @@ -253,7 +262,12 @@ const ChatInterface = ({
const newMessages = messages.concat([
{
role: "user",
content: promptStr + "\n\n # " + editorTitle + "\n\n" + editorContent,
//content: promptStr + "\n\n # " + editorTitle + "\n\n" + editorContent,
content:
"Based on the following prompt, generate a revised version of the editorContent: \n\n" +
"Prompt: " + promptStr + "\n\n" +
"Original editorContent: " + editorContent + "\n\n" +
"Please respond **only** with the updated version of the editorContent. Do not include any explanations, comments, or extra text.",
},
]);
setMessages(newMessages);
Expand Down Expand Up @@ -411,8 +425,59 @@ const ChatInterface = ({
{/* Messages */}
{messages.map((message, idx) => (
<div key={`${idx}`}>
{/* Regular message content */}
{typeof message.content == "string" &&
{/* User message */}
{typeof message.content == "string" &&message.role === "user" && (
<div
className={`${"bg-primary text-primary-foreground rounded-2xl p-3 text-left ml-auto"
} space-y-3 flex flex-col w-fit`}>
<Markdown>{message.content}</Markdown>
</div>
)}
{/* Assistant message */}
{ typeof message.content == "string" && message.role === "assistant" && (
<div
className={`group relative transition duration-200 ${
!pending && messages.at(-1) === message
? "hover:bg-indigo-100"
: ""
} bg-indigo-50 text-indigo-900 border-l-4 border-indigo-300 p-4 rounded-md text-left space-y-3 flex flex-col w-fit`}
>
{/* Message body */}
<Markdown>{message.content}</Markdown>

{/* Buttons appear only on hover, after response is done */}
{!pending && messages.at(-1) === message && (
<div className={`opacity-0 ${buttonClicked ? "" : "group-hover:opacity-100"}
transition-opacity duration-200 flex justify-end space-x-3 pt-2`}>
<button
className="text-lg hover:scale-110 transition"
title="Accept"
onClick={() => {
setButtonClicked(true);
if (typeof message.content === "string") {
setEditorContentWrapper(message.content);
toast.success("Response replaced editorContent.");
}
}}
>
</button>
<button
className="text-lg hover:scale-110 transition"
title="Reject"
onClick={() => {
setButtonClicked(true);
toast("Response rejected.");
}}
>
</button>
</div>
)}
</div>
)}

{/* {typeof message.content == "string" &&
message.role !== "tool" && (
<div
className={`${
Expand All @@ -423,7 +488,8 @@ const ChatInterface = ({
>
<Markdown>{message.content}</Markdown>
</div>
)}
)} */}

{typeof message.content == "string" &&
message.role == "tool" &&
expandingToolCalls.includes(message.tool_call_id) && (
Expand Down Expand Up @@ -498,7 +564,8 @@ const ChatInterface = ({
<div className="flex flex-grow w-full items-end space-x-2">
<Textarea
className="flex flex-1 flex-grow resize-none"
placeholder="您想根据当前文章问什么?"
//placeholder="您想根据当前文章问什么?"
placeholder="How do you want to improve the editorContent?"
value={prompt}
onChange={(e) => {
setPrompt(e.target.value);
Expand Down
23 changes: 18 additions & 5 deletions react/src/PostEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,15 @@ function useDebounce<T extends (...args: any[]) => any>(
export default function PostEditor({
curPath,
setCurPath,
editorContent,
setEditorContent,
setEditorContentWrapper,
}: {
curPath: string;
setCurPath: (path: string) => void;
editorContent: string;
setEditorContent: (content: string) => void;
setEditorContentWrapper: (content: string) => void;
}) {
const HEADER_HEIGHT = 50;
const { theme } = useTheme();
Expand All @@ -91,10 +97,17 @@ export default function PostEditor({
const [isPostMode, setIsPostMode] = useState(false);
const mdxEditorRef = useRef<MDXEditorMethods>(null);
const [editorTitle, setEditorTitle] = useState("");
const [editorContent, setEditorContent] = useState("");
//const [editorContent, setEditorContent] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);

useEffect(() => {
console.log("Received editorContent:", editorContent);
if (mdxEditorRef.current) {
mdxEditorRef.current.setMarkdown(editorContent);
}
}, [editorContent]);

useEffect(() => {
setIsLoading(true);
fetch("/api/read_file", {
Expand Down Expand Up @@ -168,10 +181,10 @@ export default function PostEditor({
debouncedRenameFile(title);
};

const setEditorContentWrapper = (content: string) => {
setEditorContent(content);
debouncedUpdateFile(content);
};
// const setEditorContentWrapper = (content: string) => {
// setEditorContent(content);
// debouncedUpdateFile(content);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个debounceUpdateFile()之后还是得加回来吧 这个是保存到磁盘上的file的

Copy link
Collaborator

@stevenxichengao stevenxichengao May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

那个setEditorContentWrapper我放到parent component里面了因为ChatInterface也需要,现在是个prop

// };

useEffect(() => {
const toolbar = document.querySelector(".my-classname");
Expand Down
92 changes: 92 additions & 0 deletions server/localmanus/routers/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,74 @@ async def execute_tool(tool_call_id: str, tool_name: str, args_str: str, session
router = APIRouter(prefix="/api")
@router.post("/chat")
async def chat(request: Request):
# data = await request.json()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里都删掉了是测试用的吗

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

对对对

# messages = data.get('messages')
# session_id = data.get('session_id')
# provider = data.get('provider')
# url = data.get('url')
# if provider == 'ollama' and not url.endswith('/v1'):
# # openai compatible url
# url = url.rstrip("/") + "/v1"
# model = data.get('model')
# if model is None:
# raise HTTPException(
# status_code=400, # Bad Request
# detail="model is required"
# )
# if provider is None:
# raise HTTPException(
# status_code=400, # Bad Request
# detail="provider is required"
# )
# if session_id is None:
# raise HTTPException(
# status_code=400, # Bad Request
# detail="session_id is required"
# )
# if len(messages) == 1:
# # create new session
# prompt = messages[0].get('content', '')
# await db_service.create_chat_session(session_id, model, provider, (prompt[:200] if isinstance(prompt, str) else ''))

# await db_service.create_message(session_id, messages[-1].get('role', 'user'), json.dumps(messages[-1])) if len(messages) > 0 else None
# # Create and store the chat task
# async def chat_loop():
# cur_messages = messages

# try:
# if cur_messages[-1].get('role') == 'assistant' and cur_messages[-1].get('tool_calls') and \
# cur_messages[-1]['tool_calls'][-1].get('function', {}).get('name') == 'finish':
# print('👇finish!')
# cur_messages.pop()
# await send_to_websocket(session_id, {
# 'type': 'all_messages',
# 'messages': cur_messages
# })

# else:
# cur_messages = await chat_openai(cur_messages, session_id, model, provider, url)
# except Exception as e:
# print(f"Error in chat_loop: {e}")
# traceback.print_exc()
# await send_to_websocket(session_id, {
# 'type': 'error',
# 'error': str(e)
# })

# await send_to_websocket(session_id, {
# 'type': 'done'
# })

# task = asyncio.create_task(chat_loop())
# stream_tasks[session_id] = task
# try:
# await task
# except asyncio.exceptions.CancelledError:
# print(f"🛑Session {session_id} cancelled during stream")
# finally:
# stream_tasks.pop(session_id, None)

# return {"status": "done"}
data = await request.json()
messages = data.get('messages')
session_id = data.get('session_id')
Expand Down Expand Up @@ -383,6 +451,30 @@ async def chat_loop():
stream_tasks.pop(session_id, None)

return {"status": "done"}
# data = await request.json()
# session_id = data.get("session_id")

# # Simulate first chunk
# await send_to_websocket(session_id, {
# "type": "delta",
# "text": "Here is a backend-"
# })

# # Simulate small delay between chunks (optional)
# await asyncio.sleep(1)

# # Simulate second chunk
# await send_to_websocket(session_id, {
# "type": "delta",
# "text": "generated assistant reply."
# })

# # Mark the assistant message as complete
# await send_to_websocket(session_id, {
# "type": "done"
# })

# return {"status": "done"}

async def send_to_websocket(session_id: str, event:dict):
ws = active_websockets.get(session_id)
Expand Down
Loading