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
56 changes: 32 additions & 24 deletions devel/0166.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,58 @@
# [0166] 聊天会话导出功能
# [0166] AI Chat 侧边栏 Dock 模式

## 1 相关文档
- [dddd.md](dddd.md) - 任务文档模板

## 2 任务相关的代码文件
- `src/Plugins/Qt/qt_chat_tab_widget.hpp` - ChatSidebar 信号声明
- `src/Plugins/Qt/qt_chat_tab_widget.cpp` - 侧边栏菜单项
- `src/Plugins/Qt/qt_chat_controller.hpp` - Controller 处理函数声明
- `src/Plugins/Qt/qt_chat_controller.cpp` - Controller 处理函数实现
- `TeXmacs/progs/dynamic/chat-session-persist.scm` - Scheme 导出函数
- `src/Plugins/Qt/qt_chat_tab_widget.hpp` - ChatTabWidget 侧边栏状态控制接口
- `src/Plugins/Qt/qt_chat_tab_widget.cpp` - 关闭侧边栏按钮及状态切换实现
- `src/Plugins/Qt/qt_tm_widget.hpp` - 主窗口 Dock 相关成员声明
- `src/Plugins/Qt/qt_tm_widget.cpp` - AI Chat 侧边栏 Dock 模式同步逻辑
- `tests/Plugins/Qt/qt_chat_tab_widget_test.cpp` - 单元测试

## 3 如何测试

### 3.1 确定性测试(单元测试)
```bash
xmake b qt_chat_tab_widget_test
xmake r qt_chat_tab_widget_test
```

### 3.2 非确定性测试(文档验证)
1. 启动 Mogan,打开 Chat 标签页
2. 进行一次对话(发送消息并等待回复完成)
3. 在侧边栏点击该会话的 "..." 按钮
4. 确认菜单中出现 "Export" 选项(位于 Rename 和 Archive 之间
5. 点击 Export,在文件对话框中选择保存位置和文件名
6. 确认指定路径下生成了 .tmu 文件,内容为会话消息
7. 确认导出的文档出现在最近文档列表中(启动页或 File → Recent 菜单可见)
1. 启动 Mogan,确保有已创建的会话
2. 在文档区域右上角确认出现 "Open AI Chat"(已有会话)或 "New AI Chat"(无会话)浮动按钮
3. 点击该按钮,确认右侧弹出侧边栏 Dock,宽度约为窗口 1/4
4. 确认编辑器或 PDF 阅读器仍然可见,聊天界面只显示对话区域(隐藏内部会话列表
5. 确认对话区域左上角出现关闭侧边栏按钮,点击后侧边栏收起
6. 通过 File → Open 或工具栏打开一个 Chat 文件(`.tm` 或特定聊天文件),确认侧边栏自动收起并进入全屏聊天模式
7. 关闭聊天文件回到编辑器,确认浮动按钮重新出现

## 4 如何提交

提交前执行以下最少步骤:

```bash
xmake b stem
xmake b qt_chat_tab_widget_test
xmake r qt_chat_tab_widget_test
```

## 5 What
为 AI 聊天界面的会话列表增加导出子功能
为 AI Chat 增加侧边栏 Dock 模式,允许在编辑或阅读 PDF 的同时侧边显示聊天界面

1. 在侧边栏会话项的 "..." 菜单中增加 "Export" 选项
2. 点击后弹出文件保存对话框,用户选择目标路径
3. 将会话 message buffer 以 TMU 格式保存到用户指定位置
4. 导出后将文档加入最近文档列表
1. 在文档区域右上角增加浮动按钮,用于打开/关闭 AI Chat 侧边栏
2. 聊天界面以右侧 QDockWidget 形式嵌入,与编辑器/PDF 阅读器共存
3. Dock 模式下隐藏聊天界面内部的会话列表,只显示对话区域
4. 对话区域左上角增加关闭侧边栏按钮
5. 全屏聊天模式(Chat Tab)与侧边栏模式互斥,切换时自动重置状态

## 6 Why
用户需要将聊天会话内容导出为独立的 TMU 文档,以便在其他地方使用或备份
用户希望在使用编辑器或阅读 PDF 时,能够随时查看和继续 AI 对话,而不必切换到全屏聊天标签页

## 7 How
1. 在 `ChatSidebar` 添加 `exportRequested` 信号
2. 在 `createItem` 的菜单中添加 "Export" 动作
3. 在 `ChatController` 中实现 `onExportRequested`:使用 `QFileDialog::getSaveFileName` 获取目标路径,调用 Scheme 函数完成导出
4. 在 `chat-session-persist.scm` 中添加 `chat-persist-export-session-to`:先 `buffer-export` 保存 buffer 到磁盘,再 `system-copy` 复制到目标路径
1. 在 `qt_tm_widget_rep` 中增加 `chatSideDock`(QDockWidget)和 `chatSidebarToggleBtn`(浮动按钮)
2. `sync_chat_sidebar_mode()` 控制侧边栏模式的显隐:将 `chatContentWidget` 移入/移出 Dock,同时隐藏/显示内部会话列表
3. 在 `QTChatTabWidget` 中增加 `setSidebarVisible()`(dock 模式专用,不触发浮动按钮)和 `setCloseSidebarButtonVisible()`
4. `update_visibility()` 中根据当前模式控制浮动按钮显隐和 tooltip(根据是否有会话动态显示 "Open AI Chat" 或 "New AI Chat")
5. 连接 `QTChatTabWidget::closeSidebarRequested` 信号到关闭侧边栏逻辑
6. `sync_chat_tab_mode()` 中若之前处于侧边栏模式,先关闭以进入全屏聊天
41 changes: 41 additions & 0 deletions src/Plugins/Qt/qt_chat_tab_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,27 @@ QTChatTabWidget::setup_right_content (QHBoxLayout* mainLayout) {

mainLayout->addWidget (content, 1);

// 对话区域左上角关闭侧边栏按钮(dock 模式使用)
closeSidebarBtn_= new QPushButton (content);
closeSidebarBtn_->setObjectName ("chat-tab-close-sidebar-btn");
closeSidebarBtn_->setFocusPolicy (Qt::NoFocus);
closeSidebarBtn_->setCursor (Qt::PointingHandCursor);
closeSidebarBtn_->setIcon (QIcon (":llm-chat/sidebar.svg"));
closeSidebarBtn_->setIconSize (QSize (DpiUtils::scaled (kToggleIconSize),
DpiUtils::scaled (kToggleIconSize)));
closeSidebarBtn_->setFixedSize (DpiUtils::scaled (kToggleBtnSize),
DpiUtils::scaled (kToggleBtnSize));
closeSidebarBtn_->setStyleSheet (
QString ("QPushButton { border: none; border-radius: %1px; "
"background: transparent; }"
"QPushButton:hover { background: rgba(0,0,0,0.08); }")
.arg (DpiUtils::scaled (kToggleBtnSize / 2)));
closeSidebarBtn_->move (DpiUtils::scaled (kFloatingBtnMarginX),
DpiUtils::scaled (kFloatingBtnMarginY));
connect (closeSidebarBtn_, &QPushButton::clicked, this,
[this] () { emit closeSidebarRequested (); });
closeSidebarBtn_->hide ();

// 浮球按钮容器
QWidget* floatingContainer= new QWidget (this);
floatingContainer->setObjectName ("chat-tab-floating-container");
Expand Down Expand Up @@ -1307,6 +1328,26 @@ QTChatTabWidget::toggle_sidebar () {
}
}

void
QTChatTabWidget::setSidebarCollapsed (bool collapsed) {
if (sidebarCollapsed_ == collapsed) return;
toggle_sidebar ();
}

void
QTChatTabWidget::setSidebarVisible (bool visible) {
if (!sidebarWidget_) return;
sidebarWidget_->setVisible (visible);
sidebarCollapsed_= !visible;
// dock 模式下不需要浮动按钮,始终隐藏
if (floatingBtnContainer_) floatingBtnContainer_->hide ();
}

void
QTChatTabWidget::setCloseSidebarButtonVisible (bool visible) {
if (closeSidebarBtn_) closeSidebarBtn_->setVisible (visible);
}

/******************************************************************************
* QTChatTabWidget 事件处理
******************************************************************************/
Expand Down
17 changes: 17 additions & 0 deletions src/Plugins/Qt/qt_chat_tab_widget.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,30 @@ class QTChatTabWidget : public QWidget {
ChatSidebar* sidebar () const { return sidebar_; }
QPushButton* newChatButton () const { return newChatButton_; }
QPushButton* floatingNewChatButton () const { return floatingNewChatBtn_; }
QPushButton* closeSidebarButton () const { return closeSidebarBtn_; }
QList<ChatConversationPanel*>& conversations () { return conversations_; }
ChatConversationPanel* activeConversation () const {
return activeConversation_;
}
void setSidebarCollapsed (bool collapsed);
bool isSidebarCollapsed () const { return sidebarCollapsed_; }
bool isSidebarWidgetVisible () const {
return sidebarWidget_ != nullptr && sidebarWidget_->isVisible ();
}
bool isFloatingContainerVisible () const {
return floatingBtnContainer_ != nullptr &&
floatingBtnContainer_->isVisible ();
}
/**
* @brief 直接设置内部侧边栏显隐(dock 模式使用,不触发浮动按钮)。
*/
void setSidebarVisible (bool visible);
void setCloseSidebarButtonVisible (bool visible);

signals:
void cancelRequested (const string& sessionId);
void newChatRequested ();
void closeSidebarRequested ();

protected:
/// 键盘事件处理(Ctrl+N 新建会话等)
Expand Down Expand Up @@ -347,6 +363,7 @@ class QTChatTabWidget : public QWidget {
QPushButton* floatingNewChatBtn_ = nullptr; ///< 浮动新建按钮
QWidget* floatingBtnContainer_= nullptr; ///< 浮动按钮容器
QPushButton* newChatButton_ = nullptr; ///< 侧边栏新建按钮
QPushButton* closeSidebarBtn_ = nullptr; ///< 对话区域关闭侧边栏按钮
QWidget* sidebarNormalContent_= nullptr; ///< 侧边栏常规内容区
QStackedWidget* conversationStack_ = nullptr; ///< 会话面板堆栈

Expand Down
Loading
Loading