Skip to content
Merged
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
4 changes: 3 additions & 1 deletion libs/hexagent_demo/backend/hexagent_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class AppConfig:
sandbox: SandboxConfig = field(default_factory=SandboxConfig)
disabled_skills: list[str] = field(default_factory=list)
mcp_servers: list[McpServerConfig] = field(default_factory=list)
language: str = "en"

@property
def main_model(self) -> ModelConfig | None:
Expand Down Expand Up @@ -120,7 +121,8 @@ def load_config() -> AppConfig:
sandbox = SandboxConfig(**file_data["sandbox"]) if "sandbox" in file_data else SandboxConfig()
disabled_skills = file_data.get("disabled_skills", [])
mcp_servers = [McpServerConfig(**m) for m in file_data.get("mcp_servers", [])]
return AppConfig(models=models, main_model_id=main_id, fast_model_id=fast_id, agents=agents, tools=tools, sandbox=sandbox, disabled_skills=disabled_skills, mcp_servers=mcp_servers)
language = file_data.get("language", "en")
return AppConfig(models=models, main_model_id=main_id, fast_model_id=fast_id, agents=agents, tools=tools, sandbox=sandbox, disabled_skills=disabled_skills, mcp_servers=mcp_servers, language=language)

# No config.json — return empty config (frontend will show setup flow)
config = AppConfig()
Expand Down
3 changes: 3 additions & 0 deletions libs/hexagent_demo/backend/hexagent_api/routes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ async def put_config(body: dict[str, Any]) -> dict[str, Any]:
if "mcp_servers" in body:
current.mcp_servers = [McpServerConfig(**m) for m in body["mcp_servers"]]

if "language" in body:
current.language = body["language"]

save_config(current)

# Invalidate cached agents so they pick up new config on next use
Expand Down
95 changes: 91 additions & 4 deletions libs/hexagent_demo/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions libs/hexagent_demo/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
"dependencies": {
"docx-preview": "^0.3.7",
"echarts": "^6.0.0",
"i18next": "^25.10.10",
"lucide-react": "^0.577.0",
"mermaid": "^11.13.0",
"pdfjs-dist": "^5.4.296",
"pptx-viewer": "^0.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^17.0.0",
"react-markdown": "^9.0.1",
"react-pdf": "^10.4.1",
"react-syntax-highlighter": "^16.1.1",
Expand Down
9 changes: 7 additions & 2 deletions libs/hexagent_demo/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useReducer, useEffect, useCallback, useRef, useState } from "react";
import i18n from "./i18n";
import { AppContext, initialState, reducer } from "./store";
import { listConversations, createConversation, createWarmSession, deleteWarmSession, sendMessage, subscribeToStream, getServerConfig, getVMStatus, type ServerConfig } from "./api";
import { useSettings } from "./hooks/useSettings";
Expand Down Expand Up @@ -86,6 +87,10 @@ function App() {
loadedConfig = cfg;
dispatch({ type: "SET_SERVER_CONFIG", payload: cfg });
if (cfg.models.length === 0) setSetupNeeded(true);
// Sync language from backend config if it differs from local
if (cfg.language && cfg.language !== settings.language) {
setSettings((prev) => ({ ...prev, language: cfg.language }));
}
})
.catch(() => {});
Promise.all([convP, cfgP]).then(() => {
Expand Down Expand Up @@ -202,7 +207,7 @@ function App() {
warmSessionPromiseRef.current = p;
p.catch(() => {
dispatch({ type: "SHOW_NOTIFICATION", payload: {
message: "Session setup failed. You can still send messages.",
message: i18n.t("misc:sessionSetupFailed"),
type: "info",
}});
})
Expand Down Expand Up @@ -418,7 +423,7 @@ function App() {
doSendMessage(conv.id, content, options?.attachments);
} catch {
dispatch({ type: "REQUEST_END" });
dispatch({ type: "SHOW_NOTIFICATION", payload: { message: "Failed to create conversation", type: "error" } });
dispatch({ type: "SHOW_NOTIFICATION", payload: { message: i18n.t("misc:failedToCreateConversation"), type: "error" } });
}
},
[dispatch, state.activeConversationId, state.isRequestPending, state.streamingByConversation, state.selectedModelId, state.selectedMode, state.warmSessionId, doSendMessage]
Expand Down
21 changes: 11 additions & 10 deletions libs/hexagent_demo/frontend/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Conversation } from "./types";
import type { Conversation } from "./types";

const API_BASE = (() => {
if (typeof window !== 'undefined' && window.electronAPI?.backendPort) {
Expand Down Expand Up @@ -34,7 +34,7 @@ export async function getConversation(id: string): Promise<Conversation> {
return res.json();
}

// ── Warm session (pre-conversation) ──
// 鈹€鈹€ Warm session (pre-conversation) 鈹€鈹€

export interface WarmSessionResponse {
session_id: string;
Expand Down Expand Up @@ -264,7 +264,7 @@ export function subscribeToStream(
return controller;
}

// ── File upload ──
// 鈹€鈹€ File upload 鈹€鈹€

export interface UploadResult {
filename: string;
Expand Down Expand Up @@ -295,7 +295,7 @@ export async function deleteChatFile(conversationId: string, filename: string):
}
}

// ── Folder picker ──
// 鈹€鈹€ Folder picker 鈹€鈹€

export async function browseFolder(): Promise<string | null> {
const res = await fetch(`${API_BASE}/api/browse-folder`, { method: "POST" });
Expand All @@ -304,7 +304,7 @@ export async function browseFolder(): Promise<string | null> {
return data.path || null;
}

// ── Server config ──
// 鈹€鈹€ Server config 鈹€鈹€

export interface ModelConfig {
id: string;
Expand Down Expand Up @@ -359,13 +359,14 @@ export interface ServerConfig {
tools: ToolsConfig;
sandbox: SandboxConfig;
mcp_servers: McpServerEntry[];
language: string;
}

export async function getServerConfig(): Promise<ServerConfig> {
const res = await fetch(`${API_BASE}/api/config`);
if (!res.ok) throw new Error(`Failed to get config: ${res.statusText}`);
const data = await res.json();
return { agents: [], tools: { search_provider: "", search_api_key: "", fetch_provider: "jina", fetch_api_key: "" }, sandbox: { e2b_api_key: "", chat_enabled: false }, mcp_servers: [], ...data };
return { agents: [], tools: { search_provider: "", search_api_key: "", fetch_provider: "jina", fetch_api_key: "" }, sandbox: { e2b_api_key: "", chat_enabled: false }, mcp_servers: [], language: "en", ...data };
}

export async function updateServerConfig(config: ServerConfig): Promise<ServerConfig> {
Expand All @@ -388,7 +389,7 @@ export async function testMcpConnection(server: McpServerEntry): Promise<{ ok: b
return res.json();
}

// ── Skills ──
// 鈹€鈹€ Skills 鈹€鈹€

export interface SkillsList {
public: string[];
Expand Down Expand Up @@ -446,7 +447,7 @@ export async function toggleSkill(name: string, enabled: boolean): Promise<void>
if (!res.ok) throw new Error(`Failed to toggle skill: ${res.statusText}`);
}

// ── Setup / VM backend ──
// 鈹€鈹€ Setup / VM backend 鈹€鈹€

export interface VMStatus {
supported: boolean;
Expand Down Expand Up @@ -481,7 +482,7 @@ export function installVMBackend(
});
}

// ── VM Build ──
// 鈹€鈹€ VM Build 鈹€鈹€

export interface VMBuildStatus {
status: "idle" | "running" | "done" | "error";
Expand Down Expand Up @@ -562,7 +563,7 @@ export function buildVM(
});
}

// ── VM Provision ──
// 鈹€鈹€ VM Provision 鈹€鈹€

export interface ProvisionStepDef {
id: string;
Expand Down
14 changes: 8 additions & 6 deletions libs/hexagent_demo/frontend/src/components/ChatArea.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import { PanelRight } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useAppContext } from "../store";
import { getVMStatus } from "../api";
import WelcomeScreen from "./WelcomeScreen";
Expand All @@ -18,6 +19,7 @@ interface ChatAreaProps {
}

export default function ChatArea({ conversation, onSendMessage, onOpenSettings, rightPanel }: ChatAreaProps) {
const { t } = useTranslation("chat");
const { state, dispatch } = useAppContext();
const [editingTitle, setEditingTitle] = useState(false);
const chatAreaRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -89,7 +91,7 @@ export default function ChatArea({ conversation, onSendMessage, onOpenSettings,
[dispatch]
);

// Keyboard shortcuts: Cmd/Ctrl+Shift+1 Chat, Cmd/Ctrl+Shift+2 Cowork
// Keyboard shortcuts: Cmd/Ctrl+Shift+1 鈫?Chat, Cmd/Ctrl+Shift+2 鈫?Cowork
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const mod = isMac ? e.metaKey : e.ctrlKey;
Expand Down Expand Up @@ -155,10 +157,10 @@ export default function ChatArea({ conversation, onSendMessage, onOpenSettings,
onClick={() => handleModeChange(mode)}
type="button"
>
{mode === "chat" ? "Chat" : "Cowork"}
{t(`mode.${mode}`)}
<span className="custom-tooltip">
{mode === "chat" ? "Chat" : "Cowork"}
<span className="custom-tooltip-shortcut">{isMac ? "⇧⌘" : "Ctrl+Shift+"}{idx + 1}</span>
{t(`mode.${mode}`)}
<span className="custom-tooltip-shortcut">{isMac ? "鈬р寴" : "Ctrl+Shift+"}{idx + 1}</span>
</span>
</button>
))}
Expand All @@ -170,7 +172,7 @@ export default function ChatArea({ conversation, onSendMessage, onOpenSettings,
<button
className="right-panel-toggle"
onClick={() => dispatch({ type: "SET_RIGHT_PANEL", payload: !(state.rightPanelByConversation[conversation?.id ?? ""] ?? false) })}
title="Toggle side panel"
title={t("toggleSidePanel")}
>
<PanelRight />
</button>
Expand Down
Loading
Loading