|
@@ -25,6 +25,9 @@ import { MessageBubbleComponent } from '../../../components/domain/message-bubbl
|
|
|
import { TypingIndicatorComponent } from '../../../components/domain/typing-indicator/typing-indicator';
|
|
import { TypingIndicatorComponent } from '../../../components/domain/typing-indicator/typing-indicator';
|
|
|
import { ToolActivityComponent } from '../../../components/domain/tool-activity/tool-activity';
|
|
import { ToolActivityComponent } from '../../../components/domain/tool-activity/tool-activity';
|
|
|
import { FilePreviewComponent } from '../../../components/preview/file-preview/file-preview';
|
|
import { FilePreviewComponent } from '../../../components/preview/file-preview/file-preview';
|
|
|
|
|
+import { NewSessionModalService } from '../../../components/domain/new-session-modal/new-session-modal.service';
|
|
|
|
|
+import { ImageLightboxComponent } from '../../../components/preview/image-lightbox/image-lightbox';
|
|
|
|
|
+import { previewKindOf, type PreviewKind } from '../../../components/preview/file-kind';
|
|
|
import { PresenceBadgeComponent } from '../../../components/domain/presence-badge/presence-badge';
|
|
import { PresenceBadgeComponent } from '../../../components/domain/presence-badge/presence-badge';
|
|
|
import { AgentPresence, agentPresence } from '../../../core/api/models';
|
|
import { AgentPresence, agentPresence } from '../../../core/api/models';
|
|
|
import { StatusBadgeComponent } from '../../../components/domain/status-badge/status-badge';
|
|
import { StatusBadgeComponent } from '../../../components/domain/status-badge/status-badge';
|
|
@@ -45,6 +48,7 @@ import { UnreadService } from '../../../core/unread.service';
|
|
|
import { TasksService } from '../../../core/tasks.service';
|
|
import { TasksService } from '../../../core/tasks.service';
|
|
|
import { ChatSession } from '../../../core/chat-session';
|
|
import { ChatSession } from '../../../core/chat-session';
|
|
|
import { ChatSessionService } from '../../../core/chat-session.service';
|
|
import { ChatSessionService } from '../../../core/chat-session.service';
|
|
|
|
|
+import { MatrixSessionService } from '../../../core/matrix-session.service';
|
|
|
import { WorkspaceService } from '../../../core/workspace.service';
|
|
import { WorkspaceService } from '../../../core/workspace.service';
|
|
|
import { AgentModalService } from '../../../components/domain/agent-modal/agent-modal.service';
|
|
import { AgentModalService } from '../../../components/domain/agent-modal/agent-modal.service';
|
|
|
|
|
|
|
@@ -91,6 +95,16 @@ export function clampRailWidth(w: number): number {
|
|
|
return Math.min(560, Math.max(280, Math.round(w)));
|
|
return Math.min(560, Math.max(280, Math.round(w)));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 预览形态下的右栏宽度上限(2026-10-30)。
|
|
|
|
|
+ * 看文档/表格时 560px 太窄;预览打开时放宽到 720,关闭后回到常规上限。
|
|
|
|
|
+ */
|
|
|
|
|
+export const RAIL_W_PREVIEW_MAX = 720;
|
|
|
|
|
+
|
|
|
|
|
+export function clampPreviewRailWidth(w: number): number {
|
|
|
|
|
+ return Math.min(RAIL_W_PREVIEW_MAX, Math.max(280, Math.round(Number.isFinite(w) ? w : 320)));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 左栏「容器行」—— 会话按容器分组时的分组头 */
|
|
/** 左栏「容器行」—— 会话按容器分组时的分组头 */
|
|
|
interface ConversationRow {
|
|
interface ConversationRow {
|
|
|
id: string;
|
|
id: string;
|
|
@@ -117,11 +131,12 @@ interface ConversationRow {
|
|
|
TypingIndicatorComponent,
|
|
TypingIndicatorComponent,
|
|
|
ToolActivityComponent,
|
|
ToolActivityComponent,
|
|
|
FilePreviewComponent,
|
|
FilePreviewComponent,
|
|
|
|
|
+ ImageLightboxComponent,
|
|
|
PresenceBadgeComponent,
|
|
PresenceBadgeComponent,
|
|
|
StatusBadgeComponent,
|
|
StatusBadgeComponent,
|
|
|
],
|
|
],
|
|
|
template: `
|
|
template: `
|
|
|
- <div class="conversation-layout" [style.--rail-w.px]="railWidth()">
|
|
|
|
|
|
|
+ <div class="conversation-layout" [style.--rail-w.px]="effectiveRailWidth()">
|
|
|
<!--
|
|
<!--
|
|
|
🔴 左栏(会话树)已提升到 DesktopShell 外壳组件(2026-10-30)——
|
|
🔴 左栏(会话树)已提升到 DesktopShell 外壳组件(2026-10-30)——
|
|
|
一级路由点击时左栏必须保持不变,所以它不能挂在某个页面里。
|
|
一级路由点击时左栏必须保持不变,所以它不能挂在某个页面里。
|
|
@@ -141,14 +156,12 @@ interface ConversationRow {
|
|
|
<div class="chat-title">
|
|
<div class="chat-title">
|
|
|
<strong>{{ a.name }}</strong>
|
|
<strong>{{ a.name }}</strong>
|
|
|
<!--
|
|
<!--
|
|
|
- 当前对话名。多会话后「这个容器」不再等于「这一条对话」,
|
|
|
|
|
- 必须让用户一眼看到正在聊哪一条;点标题可直接改名。
|
|
|
|
|
|
|
+ 当前任务名(只读展示,2026-10-31 用户要求取消命名与改名):
|
|
|
|
|
+ 原来这里是可点击重命名的「对话名」。现在对话由容器命名,
|
|
|
|
|
+ 用户不需要也不应该改 —— 去掉点击改名入口,只保留只读文案。
|
|
|
-->
|
|
-->
|
|
|
@if (chat.currentSession(); as s) {
|
|
@if (chat.currentSession(); as s) {
|
|
|
- <small class="ellipsis session-name" role="button" tabindex="0"
|
|
|
|
|
- title="点击重命名这条对话"
|
|
|
|
|
- (click)="startRename(s, $event)"
|
|
|
|
|
- (keydown.enter)="startRename(s, $event)">
|
|
|
|
|
|
|
+ <small class="ellipsis session-name" [title]="s.title">
|
|
|
<i class="fa-solid fa-comment-dots" aria-hidden="true"></i>
|
|
<i class="fa-solid fa-comment-dots" aria-hidden="true"></i>
|
|
|
{{ s.title }}
|
|
{{ s.title }}
|
|
|
@if (s.archived) {
|
|
@if (s.archived) {
|
|
@@ -249,7 +262,7 @@ interface ConversationRow {
|
|
|
<div class="banner-hold">
|
|
<div class="banner-hold">
|
|
|
@if (currentContainerState() === 'unreported' && chat.connection() === 'connected') {
|
|
@if (currentContainerState() === 'unreported' && chat.connection() === 'connected') {
|
|
|
<ui-alert type="warning" title="容器状态未知,可能无法应答">
|
|
<ui-alert type="warning" title="容器状态未知,可能无法应答">
|
|
|
- 已连接矩阵服务,但该容器的接入通道未上报在线状态,消息可能无人应答。
|
|
|
|
|
|
|
+ 该容器还没有上报在线状态,发给它的消息可能无人处理。
|
|
|
</ui-alert>
|
|
</ui-alert>
|
|
|
} @else {
|
|
} @else {
|
|
|
<ui-alert
|
|
<ui-alert
|
|
@@ -300,6 +313,20 @@ interface ConversationRow {
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
@case ('empty') {
|
|
@case ('empty') {
|
|
|
|
|
+ @if (!chat.currentSession()) {
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 还没有任何会话(2026-11-01 用户裁决):
|
|
|
|
|
+ 不再凭空创建「临时会话」,提示用户先新建对话再开始。
|
|
|
|
|
+ -->
|
|
|
|
|
+ <div class="no-session">
|
|
|
|
|
+ <i class="fa-solid fa-comments" aria-hidden="true"></i>
|
|
|
|
|
+ <h2 class="no-session-title">还没有对话</h2>
|
|
|
|
|
+ <p class="no-session-sub">点击左侧「新建对话」,选择一个数字生命后开始。</p>
|
|
|
|
|
+ <ui-button variant="primary" size="sm" (click)="requestNewSession()">
|
|
|
|
|
+ 新建对话
|
|
|
|
|
+ </ui-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ } @else {
|
|
|
<!--
|
|
<!--
|
|
|
空会话欢迎区(2026-10-29,参考 ChatGPT 落地页):
|
|
空会话欢迎区(2026-10-29,参考 ChatGPT 落地页):
|
|
|
给「该做什么」的具体引导,而不是一句泛泛的「还没有消息」。
|
|
给「该做什么」的具体引导,而不是一句泛泛的「还没有消息」。
|
|
@@ -331,6 +358,7 @@ interface ConversationRow {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
@default {
|
|
@default {
|
|
|
<!-- AC-04.4:向上翻历史(滚动到顶部自动加载) -->
|
|
<!-- AC-04.4:向上翻历史(滚动到顶部自动加载) -->
|
|
@@ -358,7 +386,6 @@ interface ConversationRow {
|
|
|
[message]="row.message!"
|
|
[message]="row.message!"
|
|
|
[own]="row.message!.sender === 'me'"
|
|
[own]="row.message!.sender === 'me'"
|
|
|
(retry)="chat.retry($event)"
|
|
(retry)="chat.retry($event)"
|
|
|
- (regenerate)="regenerate($event)"
|
|
|
|
|
(openTask)="openTaskRail()"
|
|
(openTask)="openTaskRail()"
|
|
|
(preview)="openFile($event)"
|
|
(preview)="openFile($event)"
|
|
|
/>
|
|
/>
|
|
@@ -389,6 +416,14 @@ interface ConversationRow {
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 🔴 没有选中会话时不显示输入区(2026-11-01 用户要求)。
|
|
|
|
|
+ 空态下仍摆一个可以打字的输入框,用户会以为「直接输入就能开始」,
|
|
|
|
|
+ 但此时没有线程根,发出去会走隐式建会话的旁路,容易产生语义混乱。
|
|
|
|
|
+ 空态只保留说明 + 「新建对话」按钮,入口唯一。
|
|
|
|
|
+ 例外:state 是 'ready'/'empty' 但有 currentSession(正常会话)→ 正常显示。
|
|
|
|
|
+ -->
|
|
|
|
|
+ @if (chat.currentSession()) {
|
|
|
<div class="composer-hold">
|
|
<div class="composer-hold">
|
|
|
<!--
|
|
<!--
|
|
|
暂存附件条:选文件后**先停在这里**,点发送才真正上传+发送。
|
|
暂存附件条:选文件后**先停在这里**,点发送才真正上传+发送。
|
|
@@ -398,23 +433,45 @@ interface ConversationRow {
|
|
|
@if (chat.pendingFiles().length > 0) {
|
|
@if (chat.pendingFiles().length > 0) {
|
|
|
<div class="pending-holds" role="list" aria-label="待发送附件">
|
|
<div class="pending-holds" role="list" aria-label="待发送附件">
|
|
|
@for (pf of chat.pendingFiles(); track pf.id) {
|
|
@for (pf of chat.pendingFiles(); track pf.id) {
|
|
|
- <span class="pending-chip" role="listitem">
|
|
|
|
|
- <!-- 图片附件显示缩略图,其它类型显示回形针 -->
|
|
|
|
|
- @if (pf.previewUrl) {
|
|
|
|
|
- <img class="pending-thumb" [src]="pf.previewUrl" alt="" aria-hidden="true" />
|
|
|
|
|
- } @else {
|
|
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 图片附件(2026-10-31 按参考图重做):
|
|
|
|
|
+ 大缩略图 + 下方「解释图片 →」+ 右上角移除。
|
|
|
|
|
+ 点缩略图直接打开图片预览(灯箱)。
|
|
|
|
|
+ -->
|
|
|
|
|
+ @if (pf.previewUrl) {
|
|
|
|
|
+ <div class="pending-card" role="listitem">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="pending-card-img"
|
|
|
|
|
+ [attr.aria-label]="'预览 ' + pf.file.name"
|
|
|
|
|
+ (click)="previewPending(pf)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img [src]="pf.previewUrl" [alt]="pf.file.name" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <span class="pending-card-act" aria-hidden="true">解释图片 →</span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="pending-x pending-x--float"
|
|
|
|
|
+ [attr.aria-label]="'移除 ' + pf.file.name"
|
|
|
|
|
+ (click)="chat.unstageFile(pf.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ } @else {
|
|
|
|
|
+ <span class="pending-chip" role="listitem">
|
|
|
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
|
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
|
|
- }
|
|
|
|
|
- <span class="pending-name ellipsis" [title]="pf.file.name">{{ pf.file.name }}</span>
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- class="pending-x"
|
|
|
|
|
- [attr.aria-label]="'移除 ' + pf.file.name"
|
|
|
|
|
- (click)="chat.unstageFile(pf.id)"
|
|
|
|
|
- >
|
|
|
|
|
- <i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
|
|
|
- </button>
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ <span class="pending-name ellipsis" [title]="pf.file.name">{{ pf.file.name }}</span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="pending-x"
|
|
|
|
|
+ [attr.aria-label]="'移除 ' + pf.file.name"
|
|
|
|
|
+ (click)="chat.unstageFile(pf.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
@@ -430,6 +487,7 @@ interface ConversationRow {
|
|
|
(attach)="chat.stageFiles($event)"
|
|
(attach)="chat.stageFiles($event)"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
@@ -499,23 +557,12 @@ interface ConversationRow {
|
|
|
</div>
|
|
</div>
|
|
|
<div class="rail-body" [class.has-preview]="!!previewFile()">
|
|
<div class="rail-body" [class.has-preview]="!!previewFile()">
|
|
|
<!--
|
|
<!--
|
|
|
- 文件预览(2026-10-30):从"全屏遮罩弹窗"改为**右栏内联**。
|
|
|
|
|
- 看文件时往往还想对照对话内容,遮罩会把正文盖住。
|
|
|
|
|
- 内联模式复用同一个 app-file-preview(inline=true 时去掉遮罩与居中)。
|
|
|
|
|
|
|
+ 文件预览(2026-10-30 重构):右栏内联,头部自带「右侧 | 全屏」切换。
|
|
|
|
|
+ 文件名与返回按钮已收进 app-file-preview 头部,这里不再重复一行。
|
|
|
|
|
+ 图片不进右栏(打开即走深色灯箱),见下方 app-image-lightbox。
|
|
|
-->
|
|
-->
|
|
|
@if (previewFile(); as pf) {
|
|
@if (previewFile(); as pf) {
|
|
|
<div class="rail-preview">
|
|
<div class="rail-preview">
|
|
|
- <header class="rail-preview-head">
|
|
|
|
|
- <span class="ellipsis" [title]="pf.name">{{ pf.name }}</span>
|
|
|
|
|
- <ui-button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- shape="circle"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- icon="fa-solid fa-arrow-left"
|
|
|
|
|
- ariaLabel="返回文件列表"
|
|
|
|
|
- (click)="previewFile.set(null)"
|
|
|
|
|
- />
|
|
|
|
|
- </header>
|
|
|
|
|
<app-file-preview
|
|
<app-file-preview
|
|
|
[open]="true"
|
|
[open]="true"
|
|
|
[inline]="true"
|
|
[inline]="true"
|
|
@@ -625,7 +672,13 @@ interface ConversationRow {
|
|
|
<p class="rail-empty">该数字生命暂无交付成果。</p>
|
|
<p class="rail-empty">该数字生命暂无交付成果。</p>
|
|
|
} @else {
|
|
} @else {
|
|
|
@for (d of agentDeliverables(); track d.id) {
|
|
@for (d of agentDeliverables(); track d.id) {
|
|
|
- <div class="rail-item">
|
|
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 🔴 与「本会话文件」同一套卡片与交互(2026-11-01 用户要求):
|
|
|
|
|
+ 整行可点 = 直接打开预览,右侧给出明确的进入箭头。
|
|
|
|
|
+ 原来是 div + 右侧两个小圆钮,点了不明显、和文件栏观感割裂。
|
|
|
|
|
+ 下载仍保留为独立按钮(点行是预览,点下载是保存,语义不混)。
|
|
|
|
|
+ -->
|
|
|
|
|
+ <button type="button" class="rail-item" (click)="preview(d)">
|
|
|
<span class="file-icon" aria-hidden="true">
|
|
<span class="file-icon" aria-hidden="true">
|
|
|
<i class="fa-solid" [class]="fileIcon(d.fileType)"></i>
|
|
<i class="fa-solid" [class]="fileIcon(d.fileType)"></i>
|
|
|
</span>
|
|
</span>
|
|
@@ -654,27 +707,8 @@ interface ConversationRow {
|
|
|
</small>
|
|
</small>
|
|
|
}
|
|
}
|
|
|
</span>
|
|
</span>
|
|
|
- <span class="rail-actions">
|
|
|
|
|
- <ui-button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- shape="circle"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- icon="fa-solid fa-eye"
|
|
|
|
|
- ariaLabel="预览"
|
|
|
|
|
- (click)="preview(d)"
|
|
|
|
|
- />
|
|
|
|
|
- @if (d.artifacts[0]?.url; as url) {
|
|
|
|
|
- <a
|
|
|
|
|
- class="dl-btn"
|
|
|
|
|
- [href]="url"
|
|
|
|
|
- [attr.download]="d.artifacts[0].name"
|
|
|
|
|
- aria-label="下载"
|
|
|
|
|
- >
|
|
|
|
|
- <i class="fa-solid fa-download" aria-hidden="true"></i>
|
|
|
|
|
- </a>
|
|
|
|
|
- }
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
|
|
|
|
|
+ </button>
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</section>
|
|
</section>
|
|
@@ -685,24 +719,45 @@ interface ConversationRow {
|
|
|
}
|
|
}
|
|
|
</div>
|
|
</div>
|
|
|
<!--
|
|
<!--
|
|
|
- 右栏成果预览。
|
|
|
|
|
-
|
|
|
|
|
- 🔴 以前这里**另写了一套 ui-modal**,只认 demo 数据里的 content 字段;
|
|
|
|
|
- 真实成果没有 content → 弹窗里只剩一个下载按钮(用户实测:「点击预览无法预览」)。
|
|
|
|
|
- 现在统一走 app-file-preview(图片/视频/PDF/DOCX/XLSX/HTML/MD/文本 都支持)。
|
|
|
|
|
|
|
+ 成果预览(2026-10-31):有 url 的走右栏内联(见 preview());
|
|
|
|
|
+ 这里只留「产物还没上云、暂无 url」的说明弹窗 —— 不假装能预览。
|
|
|
-->
|
|
-->
|
|
|
- <app-file-preview
|
|
|
|
|
|
|
+ <ui-modal
|
|
|
[open]="!!previewing()"
|
|
[open]="!!previewing()"
|
|
|
- [url]="previewing()?.artifacts?.[0]?.url ?? ''"
|
|
|
|
|
- [name]="previewing()?.filename ?? previewing()?.title ?? ''"
|
|
|
|
|
- [mimeType]="''"
|
|
|
|
|
- (closed)="previewing.set(null)"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ title="这份成果还没有可预览的文件"
|
|
|
|
|
+ (openChange)="previewing.set(null)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <p class="preview-pending-hint">
|
|
|
|
|
+ 容器还在把文件搬到云盘,完成后可以在右栏「成果」里直接预览。
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </ui-modal>
|
|
|
|
|
|
|
|
<!--
|
|
<!--
|
|
|
消息附件预览已改为**右栏内联**(2026-10-30),不再使用全屏遮罩弹窗。
|
|
消息附件预览已改为**右栏内联**(2026-10-30),不再使用全屏遮罩弹窗。
|
|
|
见右栏 .rail-preview 区块。
|
|
见右栏 .rail-preview 区块。
|
|
|
|
|
+
|
|
|
|
|
+ 图片 / 音频 / 视频不进右栏,由 openFile 分流到下面两个轻量浮层:
|
|
|
-->
|
|
-->
|
|
|
|
|
+ <app-image-lightbox
|
|
|
|
|
+ [open]="!!lightboxFile()"
|
|
|
|
|
+ [url]="lightboxFile()?.url ?? ''"
|
|
|
|
|
+ [name]="lightboxFile()?.name ?? ''"
|
|
|
|
|
+ [size]="lightboxFile()?.size ?? 0"
|
|
|
|
|
+ (closed)="lightboxFile.set(null)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <ui-modal
|
|
|
|
|
+ [open]="!!mediaFile()"
|
|
|
|
|
+ [title]="mediaFile()?.name ?? ''"
|
|
|
|
|
+ (openChange)="onMediaOpenChange($event)"
|
|
|
|
|
+ >
|
|
|
|
|
+ @if (mediaFile(); as mf) {
|
|
|
|
|
+ @if (mediaKind(mf) === 'video') {
|
|
|
|
|
+ <video class="media-modal-el" [src]="mf.url" controls playsinline></video>
|
|
|
|
|
+ } @else {
|
|
|
|
|
+ <audio class="media-modal-el media-modal-el--audio" [src]="mf.url" controls></audio>
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ </ui-modal>
|
|
|
|
|
|
|
|
<!--
|
|
<!--
|
|
|
新建对话模态框已移到外壳(app-new-session-modal)——
|
|
新建对话模态框已移到外壳(app-new-session-modal)——
|
|
@@ -728,22 +783,7 @@ interface ConversationRow {
|
|
|
</div>
|
|
</div>
|
|
|
</ui-modal>
|
|
</ui-modal>
|
|
|
|
|
|
|
|
- <!-- 重命名对话 -->
|
|
|
|
|
- @if (renaming(); as r) {
|
|
|
|
|
- <div class="rename-mask" (click)="renaming.set(null)"></div>
|
|
|
|
|
- <div class="rename-panel" role="dialog" aria-modal="true" aria-label="重命名对话">
|
|
|
|
|
- <h3>重命名对话</h3>
|
|
|
|
|
- <ui-input
|
|
|
|
|
- [value]="r.title"
|
|
|
|
|
- placeholder="给这条对话起个名字"
|
|
|
|
|
- (valueChange)="renaming.set({ id: r.id, title: $any($event) })"
|
|
|
|
|
- />
|
|
|
|
|
- <div class="rename-acts">
|
|
|
|
|
- <ui-button variant="ghost" (click)="renaming.set(null)">取消</ui-button>
|
|
|
|
|
- <ui-button variant="primary" (click)="commitRename()">保存</ui-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ <!-- 重命名弹层已移除(2026-10-31 用户要求:对话命名不可修改) -->
|
|
|
`,
|
|
`,
|
|
|
styles: [
|
|
styles: [
|
|
|
`
|
|
`
|
|
@@ -855,6 +895,20 @@ interface ConversationRow {
|
|
|
|
|
|
|
|
/* ---- 右栏标题行 ---- */
|
|
/* ---- 右栏标题行 ---- */
|
|
|
/* ---- 右栏内联预览 ---- */
|
|
/* ---- 右栏内联预览 ---- */
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 预览时 rail-body 不滚动、不留内边距(2026-10-31):
|
|
|
|
|
+ * rail-body 的滚动容器 + 预览面板的 100% 高度叠加,在浏览器整体缩放后
|
|
|
|
|
+ * 会把底部状态栏挤出可视区(用户截图)。预览面板内部自己滚动即可。
|
|
|
|
|
+ */
|
|
|
|
|
+ .rail-body.has-preview {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ /* flex 列:让 .rail-preview 撑满,而不是按内容塌缩(block 下子元素无高度) */
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 0;
|
|
|
|
|
+ align-content: normal;
|
|
|
|
|
+ }
|
|
|
.rail-preview {
|
|
.rail-preview {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
@@ -862,25 +916,22 @@ interface ConversationRow {
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
}
|
|
}
|
|
|
- .rail-preview-head {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: var(--sp-2);
|
|
|
|
|
- padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-3);
|
|
|
|
|
- border-bottom: 1px solid var(--edge);
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
|
|
+ /* 音视频模态 */
|
|
|
|
|
+ .media-modal-el {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-height: 70dvh;
|
|
|
|
|
+ border-radius: var(--r-sm);
|
|
|
|
|
+ background: var(--surface-2);
|
|
|
}
|
|
}
|
|
|
- .rail-preview-head > .ellipsis {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- font-size: var(--fs-13);
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: var(--ink);
|
|
|
|
|
|
|
+ .media-modal-el--audio {
|
|
|
|
|
+ max-height: none;
|
|
|
}
|
|
}
|
|
|
.rail-preview app-file-preview {
|
|
.rail-preview app-file-preview {
|
|
|
- flex: 1;
|
|
|
|
|
|
|
+ /* flex 链而非 display:block:块级宿主会按内容塌缩(实测面板高度随缩放变化) */
|
|
|
|
|
+ flex: 1 1 0;
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
- display: block;
|
|
|
|
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
}
|
|
|
/* 预览期间隐藏标签条 —— 已经在看具体文件了,切标签会让预览莫名消失 */
|
|
/* 预览期间隐藏标签条 —— 已经在看具体文件了,切标签会让预览莫名消失 */
|
|
|
.rail-col:has(.rail-preview) .rail-tabs {
|
|
.rail-col:has(.rail-preview) .rail-tabs {
|
|
@@ -1176,7 +1227,29 @@ interface ConversationRow {
|
|
|
margin-top: var(--sp-3);
|
|
margin-top: var(--sp-3);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* 已归档目录(独立分组,不是从列表里抹掉) */
|
|
|
|
|
|
|
+ /* 无会话空态(2026-11-01):提示先新建对话,不自动建空会话 */
|
|
|
|
|
+ .no-session {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ justify-items: center;
|
|
|
|
|
+ gap: var(--sp-3);
|
|
|
|
|
+ padding: var(--sp-8) var(--sp-4);
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ }
|
|
|
|
|
+ .no-session > i {
|
|
|
|
|
+ font-size: var(--fs-26);
|
|
|
|
|
+ color: var(--edge-strong, var(--muted));
|
|
|
|
|
+ }
|
|
|
|
|
+ .no-session-title {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: var(--fs-15);
|
|
|
|
|
+ color: var(--ink);
|
|
|
|
|
+ }
|
|
|
|
|
+ .no-session-sub {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: var(--fs-13);
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 已归档目录 */
|
|
|
|
|
|
|
|
/* 待发送附件条 */
|
|
/* 待发送附件条 */
|
|
|
.pending-holds {
|
|
.pending-holds {
|
|
@@ -1219,6 +1292,56 @@ interface ConversationRow {
|
|
|
flex: none;
|
|
flex: none;
|
|
|
background: var(--surface-2);
|
|
background: var(--surface-2);
|
|
|
}
|
|
}
|
|
|
|
|
+ /* 图片待发送卡片:缩略图铺满,操作条压底部,右上角移除 */
|
|
|
|
|
+ .pending-card {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 132px;
|
|
|
|
|
+ height: 96px;
|
|
|
|
|
+ border-radius: var(--r-md);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: var(--surface-2);
|
|
|
|
|
+ box-shadow: var(--shadow-soft, none);
|
|
|
|
|
+ }
|
|
|
|
|
+ .pending-card-img {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ border: 0;
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ cursor: zoom-in;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ }
|
|
|
|
|
+ .pending-card-act {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ min-height: 26px;
|
|
|
|
|
+ padding: 0 var(--sp-2);
|
|
|
|
|
+ font-size: var(--fs-11);
|
|
|
|
|
+ color: var(--inverse);
|
|
|
|
|
+ background: color-mix(in srgb, var(--canvas) 62%, transparent);
|
|
|
|
|
+ backdrop-filter: blur(2px);
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ .pending-x--float {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 4px;
|
|
|
|
|
+ right: 4px;
|
|
|
|
|
+ border-radius: var(--r-tag);
|
|
|
|
|
+ background: color-mix(in srgb, var(--canvas) 60%, transparent);
|
|
|
|
|
+ color: var(--inverse);
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity var(--motion-fast) var(--ease-standard);
|
|
|
|
|
+ }
|
|
|
|
|
+ .pending-card:hover .pending-x--float,
|
|
|
|
|
+ .pending-card:focus-within .pending-x--float {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
.pending-name {
|
|
.pending-name {
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
@@ -1282,53 +1405,21 @@ interface ConversationRow {
|
|
|
color: var(--faint);
|
|
color: var(--faint);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* ---------- 重命名对话 ---------- */
|
|
|
|
|
- .rename-mask {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- inset: 0;
|
|
|
|
|
- z-index: var(--z-modal);
|
|
|
|
|
- background: color-mix(in srgb, var(--canvas) 60%, transparent);
|
|
|
|
|
- }
|
|
|
|
|
- .rename-panel {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- z-index: calc(var(--z-modal) + 1);
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translate(-50%, -50%);
|
|
|
|
|
- width: min(420px, calc(100vw - var(--sp-8)));
|
|
|
|
|
- padding: var(--sp-5);
|
|
|
|
|
- border: 1px solid var(--edge);
|
|
|
|
|
- border-radius: var(--r-lg);
|
|
|
|
|
- background: var(--surface);
|
|
|
|
|
- box-shadow: var(--shadow-pop);
|
|
|
|
|
- }
|
|
|
|
|
- .rename-panel h3 {
|
|
|
|
|
- margin: 0 0 var(--sp-4);
|
|
|
|
|
- font-size: var(--fs-15);
|
|
|
|
|
- color: var(--ink);
|
|
|
|
|
- }
|
|
|
|
|
- .rename-acts {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: flex-end;
|
|
|
|
|
- gap: var(--sp-2);
|
|
|
|
|
- margin-top: var(--sp-4);
|
|
|
|
|
|
|
+ /* 头部:当前任务名(只读,2026-10-31 取消改名) */
|
|
|
|
|
+ .preview-pending-hint {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ color: var(--ink-2);
|
|
|
|
|
+ font-size: var(--fs-13);
|
|
|
|
|
+ line-height: 1.6;
|
|
|
}
|
|
}
|
|
|
- /* 头部:当前对话名(可点击重命名) */
|
|
|
|
|
.session-name {
|
|
.session-name {
|
|
|
display: inline-flex;
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: var(--sp-1);
|
|
gap: var(--sp-1);
|
|
|
- cursor: pointer;
|
|
|
|
|
- border-radius: var(--r-sm);
|
|
|
|
|
- padding: 0 var(--sp-1);
|
|
|
|
|
|
|
+ cursor: default;
|
|
|
}
|
|
}
|
|
|
.session-name:hover {
|
|
.session-name:hover {
|
|
|
color: var(--ink-2);
|
|
color: var(--ink-2);
|
|
|
- background: var(--surface-hover);
|
|
|
|
|
- }
|
|
|
|
|
- .session-name:focus-visible {
|
|
|
|
|
- outline: 2px solid var(--primary);
|
|
|
|
|
- outline-offset: 1px;
|
|
|
|
|
}
|
|
}
|
|
|
.archived-tag {
|
|
.archived-tag {
|
|
|
margin-left: var(--sp-1);
|
|
margin-left: var(--sp-1);
|
|
@@ -1817,24 +1908,6 @@ interface ConversationRow {
|
|
|
.rail-item:hover {
|
|
.rail-item:hover {
|
|
|
background: var(--surface-hover);
|
|
background: var(--surface-hover);
|
|
|
}
|
|
}
|
|
|
- .rail-actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 2px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
- .dl-btn {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- place-items: center;
|
|
|
|
|
- width: 32px;
|
|
|
|
|
- height: 32px;
|
|
|
|
|
- border-radius: var(--r-sm);
|
|
|
|
|
- color: var(--primary);
|
|
|
|
|
- text-decoration: none;
|
|
|
|
|
- }
|
|
|
|
|
- .dl-btn:hover {
|
|
|
|
|
- background: var(--surface-active);
|
|
|
|
|
- }
|
|
|
|
|
.file-icon {
|
|
.file-icon {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
place-items: center;
|
|
place-items: center;
|
|
@@ -1877,6 +1950,8 @@ interface ConversationRow {
|
|
|
export class DkChatPage {
|
|
export class DkChatPage {
|
|
|
readonly chat = inject(ChatService);
|
|
readonly chat = inject(ChatService);
|
|
|
readonly live = inject(LiveService);
|
|
readonly live = inject(LiveService);
|
|
|
|
|
+ /** 重连时重建常驻 Matrix 连接(登出/断线后需要) */
|
|
|
|
|
+ private readonly matrixSession = inject(MatrixSessionService);
|
|
|
private readonly unread = inject(UnreadService);
|
|
private readonly unread = inject(UnreadService);
|
|
|
private readonly deliverables = inject(DeliverablesService);
|
|
private readonly deliverables = inject(DeliverablesService);
|
|
|
private readonly tasks = inject(TasksService);
|
|
private readonly tasks = inject(TasksService);
|
|
@@ -1897,6 +1972,14 @@ export class DkChatPage {
|
|
|
* `undefined` = 未打开;`null` = 日常对话;`string` = 工作区 id。
|
|
* `undefined` = 未打开;`null` = 日常对话;`string` = 工作区 id。
|
|
|
*/
|
|
*/
|
|
|
readonly newSessionWorkspace = signal<string | null | undefined>(undefined);
|
|
readonly newSessionWorkspace = signal<string | null | undefined>(undefined);
|
|
|
|
|
+
|
|
|
|
|
+ /** 「新建对话」模态框(左栏按钮与空态引导共用) */
|
|
|
|
|
+ private readonly newSessionModal = inject(NewSessionModalService);
|
|
|
|
|
+
|
|
|
|
|
+ /** 空态里点「新建对话」→ 打开容器选择框 */
|
|
|
|
|
+ requestNewSession(): void {
|
|
|
|
|
+ this.newSessionModal.open(null);
|
|
|
|
|
+ }
|
|
|
readonly railOpen = signal(true);
|
|
readonly railOpen = signal(true);
|
|
|
/** 右栏内联预览的成果 */
|
|
/** 右栏内联预览的成果 */
|
|
|
readonly previewing = signal<Deliverable | null>(null);
|
|
readonly previewing = signal<Deliverable | null>(null);
|
|
@@ -1938,7 +2021,9 @@ export class DkChatPage {
|
|
|
private resizeStartX = 0;
|
|
private resizeStartX = 0;
|
|
|
private readonly onResizeMove = (e: PointerEvent) => {
|
|
private readonly onResizeMove = (e: PointerEvent) => {
|
|
|
/* 向左拖 = 变宽:宽度增长量是「起点 X - 当前 X」 */
|
|
/* 向左拖 = 变宽:宽度增长量是「起点 X - 当前 X」 */
|
|
|
- const next = clampRailWidth(this.resizeFrom + (this.resizeStartX - e.clientX));
|
|
|
|
|
|
|
+ const raw = this.resizeFrom + (this.resizeStartX - e.clientX);
|
|
|
|
|
+ /* 预览形态下放宽上限;非预览形态回到 560 */
|
|
|
|
|
+ const next = this.previewFile() ? clampPreviewRailWidth(raw) : clampRailWidth(raw);
|
|
|
this._railWidth.set(next);
|
|
this._railWidth.set(next);
|
|
|
};
|
|
};
|
|
|
private readonly onResizeEnd = () => {
|
|
private readonly onResizeEnd = () => {
|
|
@@ -1984,8 +2069,6 @@ export class DkChatPage {
|
|
|
readonly expandedAgents = signal<Set<string>>(new Set());
|
|
readonly expandedAgents = signal<Set<string>>(new Set());
|
|
|
/** 左栏:哪些容器的「已归档」目录是展开的 */
|
|
/** 左栏:哪些容器的「已归档」目录是展开的 */
|
|
|
readonly archivedOpen = signal<Set<string>>(new Set());
|
|
readonly archivedOpen = signal<Set<string>>(new Set());
|
|
|
- /** 正在重命名的会话(null = 无) */
|
|
|
|
|
- readonly renaming = signal<{ id: string; title: string } | null>(null);
|
|
|
|
|
/** 待确认移除的会话(null = 无) */
|
|
/** 待确认移除的会话(null = 无) */
|
|
|
readonly removing = signal<ChatSession | null>(null);
|
|
readonly removing = signal<ChatSession | null>(null);
|
|
|
/** 当前数字生命的任务(右栏任务面板用) */
|
|
/** 当前数字生命的任务(右栏任务面板用) */
|
|
@@ -2134,11 +2217,53 @@ export class DkChatPage {
|
|
|
* 🔴 必须同时**打开右栏**:预览已经改成右栏内联,
|
|
* 🔴 必须同时**打开右栏**:预览已经改成右栏内联,
|
|
|
* 若用户之前收起了右栏,只设 previewFile 是看不到任何反应的(实测 bug)。
|
|
* 若用户之前收起了右栏,只设 previewFile 是看不到任何反应的(实测 bug)。
|
|
|
*/
|
|
*/
|
|
|
- openFile(f: { url: string; name: string; mimeType: string }): void {
|
|
|
|
|
|
|
+ openFile(f: { url: string; name: string; mimeType: string; size?: number }): void {
|
|
|
|
|
+ const kind = previewKindOf(f.name || f.url, f.mimeType);
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 图片 / 音视频不进右栏(用户裁决 2026-10-30):
|
|
|
|
|
+ * - 图片 → 深色灯箱,占满视口
|
|
|
|
|
+ * - 音视频 → 居中模态,原生控件自带全屏
|
|
|
|
|
+ * 其余类型 → 右栏内联预览(可切全屏)。
|
|
|
|
|
+ */
|
|
|
|
|
+ if (kind === 'image') {
|
|
|
|
|
+ this.lightboxFile.set({ url: f.url, name: f.name, size: f.size ?? 0 });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (kind === 'video' || kind === 'audio') {
|
|
|
|
|
+ this.mediaFile.set({ url: f.url, name: f.name, kind });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
this.previewFile.set(f);
|
|
this.previewFile.set(f);
|
|
|
this.railOpen.set(true);
|
|
this.railOpen.set(true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /* ---- 图片灯箱 / 音视频模态(2026-10-30)---- */
|
|
|
|
|
+ readonly lightboxFile = signal<{ url: string; name: string; size: number } | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ /** 待发送图片点缩略图 → 用同一个灯箱预览(2026-10-31) */
|
|
|
|
|
+ previewPending(pf: { file: File; previewUrl?: string }): void {
|
|
|
|
|
+ const url = pf.previewUrl;
|
|
|
|
|
+ if (!url) return;
|
|
|
|
|
+ this.lightboxFile.set({ url, name: pf.file.name, size: pf.file.size });
|
|
|
|
|
+ }
|
|
|
|
|
+ readonly mediaFile = signal<{ url: string; name: string; kind: PreviewKind } | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ mediaKind(m: { kind: PreviewKind }): 'video' | 'audio' {
|
|
|
|
|
+ return m.kind === 'video' ? 'video' : 'audio';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onMediaOpenChange(open: boolean): void {
|
|
|
|
|
+ if (!open) this.mediaFile.set(null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 预览形态下的右栏显示宽度。
|
|
|
|
|
+ * 拖拽时已按预览上限夹取;这里再兜底一次,防止旧记忆值超出。
|
|
|
|
|
+ */
|
|
|
|
|
+ readonly effectiveRailWidth = computed(() =>
|
|
|
|
|
+ this.previewFile() ? clampPreviewRailWidth(this.railWidth()) : this.railWidth(),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
/** 真实未读数(UnreadService 唯一来源;不用 Agent.unread 的占位值) */
|
|
/** 真实未读数(UnreadService 唯一来源;不用 Agent.unread 的占位值) */
|
|
|
unreadOf(agentId: string): number {
|
|
unreadOf(agentId: string): number {
|
|
|
return this.unread.countOf(agentId);
|
|
return this.unread.countOf(agentId);
|
|
@@ -2263,15 +2388,17 @@ export class DkChatPage {
|
|
|
this.tasks.ensureAllLoaded(),
|
|
this.tasks.ensureAllLoaded(),
|
|
|
]);
|
|
]);
|
|
|
if (id) {
|
|
if (id) {
|
|
|
- await this.chat.open(id, sessionId ?? undefined);
|
|
|
|
|
/*
|
|
/*
|
|
|
- * 打开后清一次空壳会话。
|
|
|
|
|
- *
|
|
|
|
|
- * 反复进出/测试会留下大量 0 条消息的「新对话」,列表上看起来像重复项
|
|
|
|
|
- * (用户反馈:同一容器下出现三个一模一样的条目)。
|
|
|
|
|
- * 这里保留**当前打开的这条**(它可能正准备发第一条消息),其余空壳清掉。
|
|
|
|
|
|
|
+ * `sessionId === 'new'` = 「新建对话」的待建地址(不落任何数据)。
|
|
|
|
|
+ * 2026-11-01:新建对话不再预建空会话,而是打开一个空白待建态,
|
|
|
|
|
+ * 用户发出第一条消息时才真正创建会话。
|
|
|
*/
|
|
*/
|
|
|
- this.sessions.pruneEmpty(id, this.chat.sessionId() ?? undefined);
|
|
|
|
|
|
|
+ if (sessionId === 'new') {
|
|
|
|
|
+ const ws = this.route.snapshot.queryParamMap.get('ws');
|
|
|
|
|
+ await this.chat.startBlank(id, ws || null);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await this.chat.open(id, sessionId ?? undefined);
|
|
|
|
|
+ }
|
|
|
// 打开后内容不满一屏 → 自动补历史(懒加载的另一半)
|
|
// 打开后内容不满一屏 → 自动补历史(懒加载的另一半)
|
|
|
void this.fillViewportIfNeeded();
|
|
void this.fillViewportIfNeeded();
|
|
|
} else {
|
|
} else {
|
|
@@ -2368,27 +2495,6 @@ export class DkChatPage {
|
|
|
this.agentModal.open(agentId);
|
|
this.agentModal.open(agentId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 重新生成(气泡操作条)。
|
|
|
|
|
- *
|
|
|
|
|
- * 容器没有「撤回并重答」的接口,因此实现的语义是**要求它重做一次**:
|
|
|
|
|
- * 发出一句明确的追问,把上一轮的约束带回去。这不是「重放」,是「让它再来一遍」——
|
|
|
|
|
- * 用户点这个按钮想要的结果一致,但不会覆盖原回答(原回答可对比)。
|
|
|
|
|
- */
|
|
|
|
|
- regenerate(messageId: string): void {
|
|
|
|
|
- const msg = this.chat.messages().find((m) => m.id === messageId);
|
|
|
|
|
- if (!msg) return;
|
|
|
|
|
- // 找到这条之前最近的一条己方消息,作为「原来要求它做什么」的依据
|
|
|
|
|
- const list = this.chat.messages();
|
|
|
|
|
- const idx = list.findIndex((m) => m.id === messageId);
|
|
|
|
|
- const prevAsk = [...list.slice(0, idx)].reverse().find((m) => m.sender === 'me');
|
|
|
|
|
- const ask = prevAsk?.text?.trim();
|
|
|
|
|
- const text = ask
|
|
|
|
|
- ? `上一条答复我不满意,请基于下面的要求重新做一遍(不要引用之前的结论,直接给新结果):\n\n${ask}`
|
|
|
|
|
- : '上一条答复我不满意,请重新做一遍,直接给新结果。';
|
|
|
|
|
- void this.chat.sendText(text);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 空会话欢迎区:按小时问候 + 4 条起点建议。
|
|
* 空会话欢迎区:按小时问候 + 4 条起点建议。
|
|
|
*
|
|
*
|
|
@@ -2455,34 +2561,16 @@ export class DkChatPage {
|
|
|
newSession(agentId: string, event?: Event, workspaceId: string | null = null): void {
|
|
newSession(agentId: string, event?: Event, workspaceId: string | null = null): void {
|
|
|
event?.stopPropagation();
|
|
event?.stopPropagation();
|
|
|
/*
|
|
/*
|
|
|
- * 🔴 已经有**空对话**时直接复用,不再新建。
|
|
|
|
|
|
|
+ * 「新建对话」**不创建任何数据**,只是把界面切到该容器的空白待建态
|
|
|
|
|
+ * (2026-11-01 用户裁决:首次发消息才建会话)。
|
|
|
*
|
|
*
|
|
|
- * 原实现每次点「+」都建一条 —— 用户反复点几下就出现多个「新对话」,
|
|
|
|
|
- * 看起来像重复数据(实测:同一容器下三条一样的条目)。
|
|
|
|
|
- * 空对话(0 条消息)没有任何内容可丢,复用它才是用户预期。
|
|
|
|
|
|
|
+ * 这样点几次「+」就留几条空壳的问题从根上消失 —— 旧实现要么建空会话、
|
|
|
|
|
+ * 要么得靠 `pruneEmpty` 事后清理,两者都会出错。
|
|
|
|
|
+ * 工作区归属通过 URL 传过去,由聊天页在会话建出来那一刻落地。
|
|
|
*/
|
|
*/
|
|
|
- const existingEmpty = this.sessions
|
|
|
|
|
- .listFor(agentId)
|
|
|
|
|
- .find((s) => !s.legacy && (s.messageCount ?? 0) === 0);
|
|
|
|
|
- if (existingEmpty) {
|
|
|
|
|
- /*
|
|
|
|
|
- * 复用的空对话若归属不对,跟着改到目标工作区 ——
|
|
|
|
|
- * 用户是在「工作区 A」下点的「新建对话」,结果对话跑到日常去了,
|
|
|
|
|
- * 会被当成功能坏了(本轮之前就是这个表现)。
|
|
|
|
|
- */
|
|
|
|
|
- if ((existingEmpty.workspaceId ?? null) !== workspaceId) {
|
|
|
|
|
- this.sessions.moveTo(existingEmpty.id, workspaceId);
|
|
|
|
|
- }
|
|
|
|
|
- this.expandedAgents.update((set) => new Set(set).add(agentId));
|
|
|
|
|
- this.openSession(agentId, existingEmpty.id);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- // 房间 id 由 ChatService 在打开时补全;此处先用已知映射,未知则留空待补
|
|
|
|
|
- const roomId = this.sessions.latestFor(agentId)?.roomId ?? '';
|
|
|
|
|
- const s = this.sessions.create(roomId, agentId);
|
|
|
|
|
- if (workspaceId) this.sessions.moveTo(s.id, workspaceId);
|
|
|
|
|
this.expandedAgents.update((set) => new Set(set).add(agentId));
|
|
this.expandedAgents.update((set) => new Set(set).add(agentId));
|
|
|
- this.openSession(agentId, s.id);
|
|
|
|
|
|
|
+ const qs = workspaceId ? `?ws=${encodeURIComponent(workspaceId)}` : '';
|
|
|
|
|
+ void this.router.navigateByUrl(`/${this.device.mode()}/chat/${agentId}/new${qs}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 点删除 → 先弹确认(删除类操作必须二次确认) */
|
|
/** 点删除 → 先弹确认(删除类操作必须二次确认) */
|
|
@@ -2532,20 +2620,6 @@ export class DkChatPage {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 开始重命名 */
|
|
|
|
|
- startRename(s: ChatSession, event?: Event): void {
|
|
|
|
|
- event?.stopPropagation();
|
|
|
|
|
- this.renaming.set({ id: s.id, title: s.title });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /** 提交重命名 */
|
|
|
|
|
- commitRename(): void {
|
|
|
|
|
- const r = this.renaming();
|
|
|
|
|
- if (!r) return;
|
|
|
|
|
- this.sessions.rename(r.id, r.title);
|
|
|
|
|
- this.renaming.set(null);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/** 会话行的时间显示(今天显示时分,更早显示日期) */
|
|
/** 会话行的时间显示(今天显示时分,更早显示日期) */
|
|
|
sessionTime(ts: number): string {
|
|
sessionTime(ts: number): string {
|
|
|
if (!ts) return '';
|
|
if (!ts) return '';
|
|
@@ -2568,14 +2642,23 @@ export class DkChatPage {
|
|
|
await this.live.refresh();
|
|
await this.live.refresh();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 重连 / 重新加载。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 必须走 `force`:`chat.open()` 在「同一 agent + 同一会话 + state=ready」时
|
|
|
|
|
+ * 会短路返回,而断线时 state 仍是 ready —— 不 force 的话按了完全没有反应
|
|
|
|
|
+ * (用户实测:「点击这个重新连接没有任何效果」)。
|
|
|
|
|
+ */
|
|
|
reconnect(): void {
|
|
reconnect(): void {
|
|
|
const id = this.chat.agentId();
|
|
const id = this.chat.agentId();
|
|
|
- if (id) void this.chat.open(id);
|
|
|
|
|
|
|
+ if (id) void this.chat.open(id, this.chat.sessionId() || undefined, true);
|
|
|
|
|
+ // Matrix 连接也可能已经断开,顺带重建一次(幂等,已连上时直接返回)
|
|
|
|
|
+ void this.matrixSession.kick();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
retryOpen(): void {
|
|
retryOpen(): void {
|
|
|
const id = this.chat.agentId();
|
|
const id = this.chat.agentId();
|
|
|
- if (id) void this.chat.open(id);
|
|
|
|
|
|
|
+ if (id) void this.chat.open(id, this.chat.sessionId() || undefined, true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 切换右栏面板:同面板再点则隐藏 */
|
|
/** 切换右栏面板:同面板再点则隐藏 */
|
|
@@ -2609,9 +2692,23 @@ export class DkChatPage {
|
|
|
if (id) void this.router.navigate(['/', this.device.mode(), 'tasks', id]);
|
|
if (id) void this.router.navigate(['/', this.device.mode(), 'tasks', id]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 右栏内联预览 */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 成果预览(2026-10-31 用户要求):改为**右栏内联**,与消息附件共用同一个预览面板。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 原来走全屏遮罩弹窗,和右侧预览是两套观感;现在统一走 `previewFile` 这条链路。
|
|
|
|
|
+ * 产物还没有 url(仍在容器里)时如实提示,不给空白预览。
|
|
|
|
|
+ */
|
|
|
preview(d: Deliverable): void {
|
|
preview(d: Deliverable): void {
|
|
|
- this.previewing.set(d);
|
|
|
|
|
|
|
+ const url = d.artifacts?.[0]?.url ?? '';
|
|
|
|
|
+ if (!url) {
|
|
|
|
|
+ this.previewing.set(d);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.openFile({
|
|
|
|
|
+ url,
|
|
|
|
|
+ name: d.filename ?? d.title ?? '成果',
|
|
|
|
|
+ mimeType: d.artifacts?.[0]?.type ?? '',
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fileIcon(type?: string): string {
|
|
fileIcon(type?: string): string {
|