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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

## [Unreleased]

### 变更

- 对话与工作区共用一份列表:侧栏「对话」进入工作区「对话」标签;点开一条才进入聊天画布。工作区侧栏默认打开「项目」标签。
- 用户可见用词对齐:员工 = 已入编部门;同事 = 同一组织里一起聊的人;专家 / 智能助手 = 尚未入编的帮手。个性化「子智能体」改为「专项助手」。群聊选择「同事或智能助手」。
- 桌面未注册的 local guest 也能打开「模型」和「知识库」(第一台电脑免登录)。
- 组织页导入/推送后显示落地回执,循环时间线用「是/否」而不是 true/false。

### 新增

- 模型页本机大模型:Ollama 已安装但未运行时显示「启动 Ollama」(Windows 走已知安装路径的 `Ollama.exe` / `ollama app` / `ollama serve`);缺少可自动处理的依赖时提供「一键下载安装」,否则说明需手动安装;「搜索本机模型」后台扫描 `.gguf` / `.ggml`(以及明确的本地 LLM safetensors 目录),结果进入本机列表并可注册到 Ollama 供对话切换。
Expand Down
36 changes: 36 additions & 0 deletions dashboard/src/layouts/ChatIndexRedirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Navigate, useLocation } from "react-router-dom";
import { useAgent } from "../context/AgentContext";
import { CONVERSATION_LIST_PATH, chatCanvasPath } from "./conversationHome";

type ChatIntentState = {
newChat?: boolean;
prefillInput?: string;
attachKnowledgeBaseId?: string;
};

function hasChatIntent(state: unknown): boolean {
if (!state || typeof state !== "object") return false;
const next = state as ChatIntentState;
return Boolean(
next.newChat || next.prefillInput || next.attachKnowledgeBaseId,
);
}

/**
* Bare ``/chat`` is not a second conversation home.
* Intent (new chat / prefill / attach KB) opens the canvas; otherwise the
* shared workspace 对话 list.
*/
export default function ChatIndexRedirect() {
const location = useLocation();
const { activeAgentId, agents } = useAgent();
if (hasChatIntent(location.state)) {
const agentId = activeAgentId || agents[0]?.agent_id;
if (agentId) {
return (
<Navigate to={chatCanvasPath(agentId)} replace state={location.state} />
);
}
}
return <Navigate to={CONVERSATION_LIST_PATH} replace />;
}
2 changes: 1 addition & 1 deletion dashboard/src/layouts/MainLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function ChatWithKey() {
export default function MainLayout() {
const location = useLocation();
const currentPath = location.pathname;
const selectedKey = resolveSelectedKey(currentPath);
const selectedKey = resolveSelectedKey(currentPath, location.search);
const isMobile = useIsMobile();
const { layoutMode } = useLayoutMode();
useDashboardPushToast();
Expand Down
10 changes: 2 additions & 8 deletions dashboard/src/layouts/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
type NavItem,
type NavSection,
} from "./sidebarNav";
import { CONVERSATION_LIST_PATH } from "./conversationHome";
import styles from "./Sidebar.module.less";
import { typeSize } from "../utils/mobileTypeScale";
import { DESKTOP_DRAG_REGION_CLASS } from "../utils/desktopChrome";
Expand Down Expand Up @@ -467,13 +468,6 @@ export default function Sidebar({
}, [isMinimal, selectMinimalPane]);

const handleNavigate = (path: string) => {
// When navigating to /chat, preserve the current chatId in the URL so the
// Chat component is not remounted (key stays the same) and the user stays
// on their most recent conversation instead of seeing a blank welcome screen.
if (path === "/chat" && window.location.pathname.startsWith("/chat/")) {
if (isMobile) onToggle();
return;
}
navigate(path);
if (isMobile) onToggle();
};
Expand All @@ -486,7 +480,7 @@ export default function Sidebar({
window.dispatchEvent(new Event(EXPAND_CHAT_RAIL_EVENT));
setChatSidebarOpen(true);
if (!window.location.pathname.startsWith("/chat")) {
navigate("/chat");
navigate(CONVERSATION_LIST_PATH);
}
if (isMobile) onToggle();
}, [
Expand Down
31 changes: 31 additions & 0 deletions dashboard/src/layouts/conversationHome.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import {
CONVERSATION_LIST_PATH,
WORKSPACE_PROJECTS_PATH,
chatCanvasPath,
resolveWorkspaceNavKey,
workspaceViewFromSearch,
} from "./conversationHome";

describe("conversationHome", () => {
it("treats the workspace conversations tab as the chat list", () => {
expect(workspaceViewFromSearch("")).toBe("conversations");
expect(workspaceViewFromSearch("?view=projects")).toBe("projects");
expect(workspaceViewFromSearch("view=tasks")).toBe("tasks");
expect(resolveWorkspaceNavKey("/projects")).toBe("chat");
expect(resolveWorkspaceNavKey("/projects", "?view=projects")).toBe(
"projects",
);
expect(resolveWorkspaceNavKey("/projects", "?view=tasks")).toBe("projects");
expect(resolveWorkspaceNavKey("/chat/agent-1/thr_1")).toBe("chat");
expect(resolveWorkspaceNavKey("/tasks")).toBe("projects");
expect(resolveWorkspaceNavKey("/experts")).toBeNull();
});

it("opens a canvas only when an expert is known", () => {
expect(chatCanvasPath()).toBe(CONVERSATION_LIST_PATH);
expect(chatCanvasPath("exp-1")).toBe("/chat/exp-1");
expect(chatCanvasPath("exp-1", "thr_1")).toBe("/chat/exp-1/thr_1");
expect(WORKSPACE_PROJECTS_PATH).toBe("/projects?view=projects");
});
});
41 changes: 41 additions & 0 deletions dashboard/src/layouts/conversationHome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/** Shared conversation list lives under the workspace 对话 tab. */
export const CONVERSATION_LIST_PATH = "/projects";
export const WORKSPACE_PROJECTS_PATH = "/projects?view=projects";
export const WORKSPACE_TASKS_PATH = "/projects?view=tasks";

export type WorkspaceView = "conversations" | "projects" | "tasks";

export function workspaceViewFromSearch(search = ""): WorkspaceView {
const raw = search.startsWith("?") ? search.slice(1) : search;
const view = new URLSearchParams(raw).get("view");
if (view === "projects" || view === "tasks") return view;
return "conversations";
}

/** Sidebar key for the unified conversation / workspace hub. */
export function resolveWorkspaceNavKey(
pathname: string,
search = "",
): "chat" | "projects" | null {
if (pathname === "/tasks" || pathname.startsWith("/tasks/")) {
return "projects";
}
if (pathname === "/projects" || pathname.startsWith("/projects/")) {
return workspaceViewFromSearch(search) === "conversations"
? "chat"
: "projects";
}
if (pathname === "/chat" || pathname.startsWith("/chat/")) {
return "chat";
}
return null;
}

export function chatCanvasPath(
agentId?: string | null,
threadId?: string | null,
): string {
if (agentId && threadId) return `/chat/${agentId}/${threadId}`;
if (agentId) return `/chat/${agentId}`;
return CONVERSATION_LIST_PATH;
}
35 changes: 34 additions & 1 deletion dashboard/src/layouts/sidebarNav.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("sidebarNav", () => {
]);
});

it("hides models and knowledge when the user lacks those permissions", () => {
it("hides models and knowledge when a non-desktop user lacks those permissions", () => {
const guest = {
id: 2,
username: "guest",
Expand All @@ -58,8 +58,41 @@ describe("sidebarNav", () => {
expect(keys).toContain("experts");
});

it("shows models and knowledge for the first-run desktop guest", () => {
const desktopGuest = {
id: 3,
username: "local",
role: "user",
permissions: [],
is_local: true,
} as OctopUser;
const items = buildNavSections(desktopGuest).flatMap((s) => s.items);
const keys = items.map((i) => i.key);
expect(keys).toContain("models");
expect(keys).toContain("knowledge-bases");
expect(items.find((i) => i.key === "chat")?.path).toBe("/projects");
expect(items.find((i) => i.key === "projects")?.path).toBe(
"/projects?view=projects",
);
});

it("labels organization without the OS suffix", () => {
expect(zh.nav.organization).toBe("组织");
expect(en.nav.organization).toBe("Organization");
});

it("locks the employee / colleague / expert glossary", () => {
expect(zh.organization.glossary).toContain("员工:已经编入某个部门");
expect(zh.organization.glossary).toContain("同事:同一组织");
expect(zh.organization.glossary).toContain("专家 / 智能助手");
expect(zh.organization.assembleBody).toContain("员工");
expect(zh.organization.assembleBody).toContain("同事");
expect(zh.organization.assembleBody).not.toContain("出现在专家列表");
expect(zh.organization.produceTitle).toContain("专家");
expect(zh.chat.expertPicker).toContain("同事或智能助手");
expect(zh.projects.groupMembers).toContain("同事或智能助手");
expect(en.organization.glossary).toContain("Employees: already assigned");
expect(en.organization.glossary).toContain("Colleagues: peers");
expect(en.chat.expertPicker).toContain("colleagues or smart helpers");
});
});
8 changes: 6 additions & 2 deletions dashboard/src/layouts/sidebarNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
} from "lucide-react";
import type { OctopUser } from "../api/modules/auth";
import { navAllowed } from "../utils/permissions";
import {
CONVERSATION_LIST_PATH,
WORKSPACE_PROJECTS_PATH,
} from "./conversationHome";

export const EXPANDED_WIDTH = 220;
export const COLLAPSED_WIDTH = 56;
Expand Down Expand Up @@ -54,7 +58,7 @@ export function buildNavSections(
const items: NavItem[] = [
{
key: "chat",
path: "/chat",
path: CONVERSATION_LIST_PATH,
icon: <MessageSquareText size={iconSize} strokeWidth={iconStroke} />,
labelKey: "nav.conversations",
},
Expand Down Expand Up @@ -95,7 +99,7 @@ export function buildNavSections(
});
items.push({
key: "projects",
path: "/projects",
path: WORKSPACE_PROJECTS_PATH,
icon: <FolderKanban size={iconSize} strokeWidth={iconStroke} />,
labelKey: "nav.workspace",
});
Expand Down
30 changes: 19 additions & 11 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@
"catalogEnabled": "Shown",
"catalogDisabled": "Hidden",
"catalogToggleHint": "When off, the module disappears from the openXYOS preview sidebar.",
"autoStartingSidecar": "Starting the openXYOS sidecar…"
"autoStartingSidecar": "Starting the openXYOS sidecar…",
"glossary": "Employees: already assigned to a department. Colleagues: peers in the same organization (use this word in group chat). Experts / smart helpers: not yet placed in a department — manage them on Experts.",
"receiptTitle": "What just happened",
"assembleReceipt": "Imported {{count}} employee(s) assigned to a department. In group chat call them colleagues, not experts.",
"assembleNext": "Next: open Conversations and invite colleagues to a group. Unassigned helpers stay on Experts.",
"packReceipt": "Packed and tried to push to openXYOS. Details below."
},
"systemSettings": {
"subtitle": "Workbench, remote desktop, and host administration in one place."
Expand All @@ -130,8 +135,8 @@
"createGroup": "New group",
"groupTitle": "Group name",
"groupTitlePlaceholder": "Optional, e.g. Weekly sync",
"groupMembers": "Choose experts",
"groupMembersRequired": "Select at least two experts",
"groupMembers": "Choose colleagues or smart helpers",
"groupMembersRequired": "Select at least two colleagues or smart helpers",
"groupCreated": "Group chat created",
"groupCreateFailed": "Could not create the group chat",
"kindDirect": "Direct",
Expand Down Expand Up @@ -828,7 +833,7 @@
}
},
"experts": {
"subtitle": "Spin up a pre-configured agent (with identity / soul / heartbeat baked in) from a scenario template. Each expert ships with its own system prompt — once created, just bind a provider and start it.",
"subtitle": "Experts (also called smart helpers) are not yet assigned to a department. Departmental staff are employees on Organization; in group chat call same-org peers colleagues.",
"loadFailed": "Failed to load experts",
"loadDetailFailed": "Failed to load expert details",
"share": {
Expand Down Expand Up @@ -1378,14 +1383,14 @@
"skillPicker": "Select skills",
"skillPickerSearch": "Search skills",
"skillPickerEmpty": "No skills available",
"expertPicker": "Select experts",
"expertPickerSearch": "Search experts or agents",
"expertPickerEmpty": "No other experts available",
"expertPicker": "Choose colleagues or smart helpers",
"expertPickerSearch": "Search colleagues or smart helpers",
"expertPickerEmpty": "No one to chat with yet. Add a smart helper on Experts, or import employees on Organization — then call them colleagues in the group.",
"expertPickerManage": "Manage experts",
"expertPickerCheck": "Select {{name}} for group chat",
"expertPickerGroupHint": "Check multiple experts or agents, then enter a group chat",
"expertPickerGroupHint": "Check colleagues or smart helpers, then enter a group chat and @ them",
"expertPickerEnterGroupCount": "Enter group chat ({{count}})",
"expertPickerGroupOpened": "Opened group chat",
"expertPickerGroupOpened": "Opened group chat. Type @ to mention a colleague or smart helper",
"subagentPicker": "Select subagents",
"subagentPickerSearch": "Search subagents",
"subagentPickerEmpty": "No installed subagents",
Expand Down Expand Up @@ -3244,7 +3249,10 @@
"onnxNotReady": "not ready",
"onnxCacheDir": "Cache: {{dir}}",
"onnxLocalCached": "Cached models",
"onnxQuickDownload": "Quick download from catalog…"
"onnxQuickDownload": "Quick download from catalog…",
"useInChat": "Switch in chat",
"localInstallRegistered": "Installed {{name}} and registered it. Open Conversations to switch.",
"localInstallNeedProvider": "Downloaded. Enable the Ollama provider on this page, then switch in chat."
},
"advancedSettings": {
"description": "Manage runtime configuration and environment variables.",
Expand Down Expand Up @@ -5399,7 +5407,7 @@
"pageShell": {
"experts": {
"title": "Experts",
"subtitle": "Create and manage your AI experts, or pick a scenario template to get started quickly."
"subtitle": "Experts / smart helpers are not yet in a department. Employees live on Organization; in group chat call same-org peers colleagues."
},
"tasks": {
"title": "Automation",
Expand Down
30 changes: 19 additions & 11 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@
"catalogEnabled": "已显示",
"catalogDisabled": "已隐藏",
"catalogToggleHint": "关闭后,该模块会从右侧 openXYOS 预览的侧栏消失。",
"autoStartingSidecar": "正在自动启动 openXYOS 边车…"
"autoStartingSidecar": "正在自动启动 openXYOS 边车…",
"glossary": "员工:已经编入某个部门。同事:同一组织里一起干活的人(群聊里这样叫)。专家 / 智能助手:还没编入部门的帮手,在「专家」页管理。",
"receiptTitle": "刚才的结果",
"assembleReceipt": "已导入 {{count}} 名员工(已入编部门)。群聊里请称他们为同事,不要叫专家。",
"assembleNext": "下一步:打开「对话」建群,把同事拉进来。还没入编的帮手在「专家」页。",
"packReceipt": "已打包并尝试推送到 openXYOS。下面是落地说明。"
},
"systemSettings": {
"subtitle": "工作台、远程桌面与主机管理集中在这里。"
Expand All @@ -130,8 +135,8 @@
"createGroup": "建群",
"groupTitle": "群聊名称",
"groupTitlePlaceholder": "可选,例如「产品周会」",
"groupMembers": "选择专家",
"groupMembersRequired": "请至少选择两位专家",
"groupMembers": "选择同事或智能助手",
"groupMembersRequired": "请至少选择两位(同事或智能助手)",
"groupCreated": "群聊已创建",
"groupCreateFailed": "无法创建群聊",
"kindDirect": "单聊",
Expand Down Expand Up @@ -828,7 +833,7 @@
}
},
"experts": {
"subtitle": "基于已配置的场景模板,一键创建预置身份/灵魂/心跳的智能体。每个专家自带 system prompt,新建后再为它绑定 provider 并启动即可。",
"subtitle": "专家(也叫智能助手)是还没编入部门的帮手。已入编的叫员工,在「组织」;同一组织里一起聊时叫同事。",
"loadFailed": "加载专家失败",
"loadDetailFailed": "加载详情失败",
"share": {
Expand Down Expand Up @@ -1378,14 +1383,14 @@
"skillPicker": "选择技能",
"skillPickerSearch": "搜索技能",
"skillPickerEmpty": "暂无可用技能",
"expertPicker": "选择专家",
"expertPickerSearch": "搜索专家或智能体",
"expertPickerEmpty": "暂无其他专家",
"expertPicker": "选择同事或智能助手",
"expertPickerSearch": "搜索同事或智能助手",
"expertPickerEmpty": "还没有可一起聊的人。到「专家」添加智能助手,或到「组织」导入员工后,群聊里称他们为同事。",
"expertPickerManage": "管理专家",
"expertPickerCheck": "选择 {{name}} 加入群聊",
"expertPickerGroupHint": "勾选多位专家或智能体,随时进入群聊",
"expertPickerGroupHint": "勾选同事或智能助手,进入群聊后可以 @ 他们",
"expertPickerEnterGroupCount": "进入群聊({{count}})",
"expertPickerGroupOpened": "已进入群聊",
"expertPickerGroupOpened": "已进入群聊,输入 @ 可点名同事或智能助手",
"subagentPicker": "选择子智能体",
"subagentPickerSearch": "搜索子智能体",
"subagentPickerEmpty": "暂无已安装的子智能体",
Expand Down Expand Up @@ -3241,7 +3246,10 @@
"onnxNotReady": "未就绪",
"onnxCacheDir": "缓存目录:{{dir}}",
"onnxLocalCached": "已缓存模型",
"onnxQuickDownload": "从目录快速下载…"
"onnxQuickDownload": "从目录快速下载…",
"useInChat": "去对话切换",
"localInstallRegistered": "已安装 {{name}} 并登记到模型列表。打开「对话」即可切换。",
"localInstallNeedProvider": "模型已下载。请先在本页启用 Ollama 提供商,再到对话里切换。"
},
"advancedSettings": {
"description": "管理运行配置和环境变量等高级选项。",
Expand Down Expand Up @@ -5538,7 +5546,7 @@
"pageShell": {
"experts": {
"title": "专家",
"subtitle": "创建与管理你的 AI 专家,也可从模板库挑选场景一键新建。"
"subtitle": "专家 / 智能助手:还没编入部门。员工在「组织」;群聊里对同一组织的人请称同事。"
},
"tasks": {
"title": "自动化",
Expand Down
Loading
Loading