完善应用设置与聊天状态逻辑
This commit is contained in:
@@ -15,9 +15,10 @@ QtDesktopPet 是一个基于 Qt Widgets / C++17 的 Windows 桌面宠物原型
|
|||||||
- `idle` / `talk` / `think` / `sleep` / `happy` / `drag` / `error` 状态
|
- `idle` / `talk` / `think` / `sleep` / `happy` / `drag` / `error` 状态
|
||||||
- 托盘显示、隐藏、退出
|
- 托盘显示、隐藏、退出
|
||||||
- 隐藏时暂停动画,显示时恢复动画
|
- 隐藏时暂停动画,显示时恢复动画
|
||||||
- 保存窗口位置和置顶状态
|
- 保存窗口位置、置顶、缩放和性能设置
|
||||||
- 文件日志和基础轮转
|
- 文件日志和基础轮转
|
||||||
- 设置窗口
|
- 设置窗口
|
||||||
|
- 应用设置:缩放、性能模式、隐藏暂停、懒加载
|
||||||
- AI Provider 分组配置
|
- AI Provider 分组配置
|
||||||
- Windows DPAPI 加密保存 API Key
|
- Windows DPAPI 加密保存 API Key
|
||||||
- 非 Windows 环境经用户确认后明文保存 API Key
|
- 非 Windows 环境经用户确认后明文保存 API Key
|
||||||
@@ -32,11 +33,8 @@ QtDesktopPet 是一个基于 Qt Widgets / C++17 的 Windows 桌面宠物原型
|
|||||||
尚未实现:
|
尚未实现:
|
||||||
|
|
||||||
- 设置页内 AI 连通性测试
|
- 设置页内 AI 连通性测试
|
||||||
- 缩放和性能模式 UI
|
|
||||||
- AppConfig 中缩放 / 性能字段的实际应用
|
|
||||||
- 角色导入/切换界面
|
- 角色导入/切换界面
|
||||||
- 对话历史持久化
|
- 对话历史持久化
|
||||||
- 角色包懒加载
|
|
||||||
- 打包发布脚本
|
- 打包发布脚本
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|||||||
@@ -1684,8 +1684,7 @@ MIT License 开源
|
|||||||
```text
|
```text
|
||||||
1. 设置页内 AI 连通性测试
|
1. 设置页内 AI 连通性测试
|
||||||
2. 对话历史内存上限和可选持久化
|
2. 对话历史内存上限和可选持久化
|
||||||
3. AppConfig 中缩放、性能模式等字段的实际应用
|
3. character.json 中 base、anchor、bubble offset 的解析与应用
|
||||||
4. character.json 中 base、anchor、bubble offset 的解析与应用
|
4. 角色包位置整理和角色切换
|
||||||
5. 角色包位置整理、角色切换和懒加载策略
|
5. 发布前素材授权确认与打包验证
|
||||||
6. 发布前素材授权确认与打包验证
|
|
||||||
```
|
```
|
||||||
|
|||||||
+17
-20
@@ -566,6 +566,8 @@ error :20 帧
|
|||||||
已删除临时 AI 测试入口和气泡测试入口
|
已删除临时 AI 测试入口和气泡测试入口
|
||||||
已支持 OpenAI / Google / DeepSeek / Custom 配置分 Provider 保存
|
已支持 OpenAI / Google / DeepSeek / Custom 配置分 Provider 保存
|
||||||
已移除废弃 Provider 配置入口,并在读取旧配置时清理废弃 Provider 配置
|
已移除废弃 Provider 配置入口,并在读取旧配置时清理废弃 Provider 配置
|
||||||
|
已支持应用设置页:缩放、性能模式、隐藏暂停、懒加载
|
||||||
|
已将 AppConfig 的 scale / performanceMode / pauseWhenHidden / enableLazyLoad 接入运行时
|
||||||
Windows 下 API Key 使用 DPAPI 加密保存,非 Windows 需用户确认后才允许明文保存
|
Windows 下 API Key 使用 DPAPI 加密保存,非 Windows 需用户确认后才允许明文保存
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -573,12 +575,10 @@ error :20 帧
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
1. shiroko 角色包仍位于项目根目录 shiroko/,尚未移动到 resources/characters/shiroko
|
1. shiroko 角色包仍位于项目根目录 shiroko/,尚未移动到 resources/characters/shiroko
|
||||||
2. SettingsDialog 仍是最小设置界面,尚未包含 AI 测试按钮、应用设置、角色选择、缩放和性能模式 UI
|
2. SettingsDialog 仍是最小设置界面,尚未包含 AI 测试按钮、角色选择和更完整的分区布局
|
||||||
3. ConfigManager 已有缩放和性能字段,但 PetWindow 尚未真正应用缩放、性能模式和角色选择
|
3. CharacterPackage 尚未解析并应用 character.json 中的 base、anchor、bubble offset
|
||||||
4. CharacterPackage 尚未解析并应用 character.json 中的 base、anchor、bubble offset
|
4. ConversationManager 请求上下文会截取最近 12 条历史,但内存中的 m_history 尚未做最大长度裁剪
|
||||||
5. ConversationManager 请求上下文会截取最近 12 条历史,但内存中的 m_history 尚未做最大长度裁剪
|
5. README 和开发文档已开始同步当前进度,但仍需随功能继续维护
|
||||||
6. 当前 FrameAnimator 采用当前角色包全部状态帧预加载,尚未做懒加载
|
|
||||||
7. README 和开发文档已开始同步当前进度,但仍需随功能继续维护
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -600,7 +600,11 @@ error :20 帧
|
|||||||
- 长文本流式输出期间应持续 talk
|
- 长文本流式输出期间应持续 talk
|
||||||
3. 给 ConversationManager 增加内存历史上限,避免长期对话无限增长
|
3. 给 ConversationManager 增加内存历史上限,避免长期对话无限增长
|
||||||
4. 把 AI 测试能力迁移到后续设置页,不再放在角色右键菜单
|
4. 把 AI 测试能力迁移到后续设置页,不再放在角色右键菜单
|
||||||
5. 更新设置页结构,为 AI、应用、角色、性能分区预留位置
|
5. 用户手测应用设置:
|
||||||
|
- 缩放比例
|
||||||
|
- 标准 / 低功耗性能模式
|
||||||
|
- 隐藏到托盘时暂停动画
|
||||||
|
- 动画懒加载
|
||||||
```
|
```
|
||||||
|
|
||||||
中期建议:
|
中期建议:
|
||||||
@@ -608,16 +612,10 @@ error :20 帧
|
|||||||
```text
|
```text
|
||||||
1. 完善设置界面:
|
1. 完善设置界面:
|
||||||
- AI 配置和测试
|
- AI 配置和测试
|
||||||
- 置顶、缩放、性能模式
|
|
||||||
- 角色包路径和角色切换
|
- 角色包路径和角色切换
|
||||||
2. 应用 AppConfig 中已有但尚未落地的字段:
|
2. 解析并应用角色包 base / anchor / bubble 配置
|
||||||
- scale
|
3. 评估是否移动 shiroko 到 resources/characters/shiroko
|
||||||
- performanceMode
|
4. 补一轮可重复的稳定性与性能测试记录
|
||||||
- pauseWhenHidden
|
|
||||||
- enableLazyLoad
|
|
||||||
3. 解析并应用角色包 base / anchor / bubble 配置
|
|
||||||
4. 评估是否移动 shiroko 到 resources/characters/shiroko
|
|
||||||
5. 补一轮可重复的稳定性与性能测试记录
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -628,8 +626,7 @@ error :20 帧
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
1. 是否把 shiroko 移动到 resources/characters/shiroko
|
1. 是否把 shiroko 移动到 resources/characters/shiroko
|
||||||
2. 是否保持当前“预加载全部当前角色状态帧”的策略,还是改成按状态懒加载
|
2. shiroko 素材是否允许作为正式开源发布素材继续保留在仓库中
|
||||||
3. shiroko 素材是否允许作为正式开源发布素材继续保留在仓库中
|
3. 设置页下一步先做 AI 测试入口,还是先做角色包配置
|
||||||
4. 设置页下一步先做 AI 测试入口,还是先做应用缩放 / 性能设置
|
4. 是否需要把对话历史持久化保存,还是第一版只保留内存会话
|
||||||
5. 是否需要把对话历史持久化保存,还是第一版只保留内存会话
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,28 +2,19 @@
|
|||||||
|
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
|
|
||||||
AnimationClip AnimationClip::fromState(const CharacterState &state, const QSize &targetSize)
|
AnimationClip AnimationClip::fromState(const CharacterState &state, const QSize &targetSize, bool loadFrames)
|
||||||
{
|
{
|
||||||
AnimationClip clip;
|
AnimationClip clip;
|
||||||
clip.stateName = state.name;
|
clip.stateName = state.name;
|
||||||
clip.fps = state.fps;
|
clip.fps = state.fps;
|
||||||
clip.loop = state.loop;
|
clip.loop = state.loop;
|
||||||
clip.nextState = state.nextState;
|
clip.nextState = state.nextState;
|
||||||
|
clip.m_framePaths = state.framePaths;
|
||||||
|
clip.m_targetSize = targetSize;
|
||||||
|
|
||||||
for (const QString &framePath : state.framePaths)
|
if (loadFrames)
|
||||||
{
|
{
|
||||||
QPixmap pixmap(framePath);
|
clip.loadFrames();
|
||||||
if (pixmap.isNull())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetSize.isValid())
|
|
||||||
{
|
|
||||||
pixmap = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
clip.m_frames.append(pixmap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return clip;
|
return clip;
|
||||||
@@ -31,7 +22,41 @@ AnimationClip AnimationClip::fromState(const CharacterState &state, const QSize
|
|||||||
|
|
||||||
bool AnimationClip::isValid() const
|
bool AnimationClip::isValid() const
|
||||||
{
|
{
|
||||||
return fps > 0 && !m_frames.isEmpty();
|
return fps > 0 && (!m_frames.isEmpty() || !m_framePaths.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AnimationClip::ensureLoaded()
|
||||||
|
{
|
||||||
|
if (m_frames.isEmpty())
|
||||||
|
{
|
||||||
|
loadFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
return !m_frames.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationClip::loadFrames()
|
||||||
|
{
|
||||||
|
if (!m_frames.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QString &framePath : m_framePaths)
|
||||||
|
{
|
||||||
|
QPixmap pixmap(framePath);
|
||||||
|
if (pixmap.isNull())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_targetSize.isValid())
|
||||||
|
{
|
||||||
|
pixmap = pixmap.scaled(m_targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frames.append(pixmap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QPixmap &AnimationClip::frameAt(int index) const
|
const QPixmap &AnimationClip::frameAt(int index) const
|
||||||
|
|||||||
@@ -5,14 +5,16 @@
|
|||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
class AnimationClip
|
class AnimationClip
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static AnimationClip fromState(const CharacterState &state, const QSize &targetSize);
|
static AnimationClip fromState(const CharacterState &state, const QSize &targetSize, bool loadFrames = true);
|
||||||
|
|
||||||
bool isValid() const;
|
bool isValid() const;
|
||||||
|
bool ensureLoaded();
|
||||||
const QPixmap &frameAt(int index) const;
|
const QPixmap &frameAt(int index) const;
|
||||||
int frameCount() const;
|
int frameCount() const;
|
||||||
|
|
||||||
@@ -22,5 +24,9 @@ public:
|
|||||||
QString nextState;
|
QString nextState;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void loadFrames();
|
||||||
|
|
||||||
|
QStringList m_framePaths;
|
||||||
|
QSize m_targetSize;
|
||||||
QVector<QPixmap> m_frames;
|
QVector<QPixmap> m_frames;
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-10
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
|
#include <QTextDocument>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
||||||
@@ -18,6 +18,18 @@ constexpr int MaxBubbleHeight = 220;
|
|||||||
constexpr int BubbleOffsetY = 8;
|
constexpr int BubbleOffsetY = 8;
|
||||||
constexpr int BubblePaddingWidth = 28;
|
constexpr int BubblePaddingWidth = 28;
|
||||||
constexpr int BubblePaddingHeight = 24;
|
constexpr int BubblePaddingHeight = 24;
|
||||||
|
constexpr int BubbleWidthSafetyMargin = 12;
|
||||||
|
|
||||||
|
QSizeF documentSizeForWidth(const QTextEdit *textEdit, const QString &message, int width)
|
||||||
|
{
|
||||||
|
QTextDocument document;
|
||||||
|
document.setDefaultFont(textEdit->font());
|
||||||
|
document.setDefaultTextOption(textEdit->document()->defaultTextOption());
|
||||||
|
document.setPlainText(message);
|
||||||
|
document.setDocumentMargin(textEdit->document()->documentMargin());
|
||||||
|
document.setTextWidth(width);
|
||||||
|
return document.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatBubble::ChatBubble(QWidget *parent)
|
ChatBubble::ChatBubble(QWidget *parent)
|
||||||
@@ -78,6 +90,10 @@ void ChatBubble::showMessage(const QString &message, const QPoint &anchorPositio
|
|||||||
m_textEdit->setPlainText(trimmed);
|
m_textEdit->setPlainText(trimmed);
|
||||||
|
|
||||||
const QSize bubbleSize = preferredBubbleSize(trimmed);
|
const QSize bubbleSize = preferredBubbleSize(trimmed);
|
||||||
|
const int documentWidth = bubbleSize.width() - BubblePaddingWidth;
|
||||||
|
const QSizeF documentSize = documentSizeForWidth(m_textEdit, trimmed, documentWidth);
|
||||||
|
const bool needsVerticalScroll = documentSize.height() + BubblePaddingHeight > MaxBubbleHeight;
|
||||||
|
m_textEdit->setVerticalScrollBarPolicy(needsVerticalScroll ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
|
||||||
m_textEdit->setFixedSize(bubbleSize);
|
m_textEdit->setFixedSize(bubbleSize);
|
||||||
setFixedSize(bubbleSize);
|
setFixedSize(bubbleSize);
|
||||||
updatePosition();
|
updatePosition();
|
||||||
@@ -180,17 +196,29 @@ bool ChatBubble::isUserInteractionEvent(QEvent *event) const
|
|||||||
|
|
||||||
QSize ChatBubble::preferredBubbleSize(const QString &message) const
|
QSize ChatBubble::preferredBubbleSize(const QString &message) const
|
||||||
{
|
{
|
||||||
const QFontMetrics metrics(m_textEdit->font());
|
QTextDocument singleLineDocument;
|
||||||
const int textWidth = metrics.horizontalAdvance(message);
|
singleLineDocument.setDefaultFont(m_textEdit->font());
|
||||||
const int preferredWidth = qBound(MinBubbleWidth, textWidth + BubblePaddingWidth, MaxBubbleWidth);
|
singleLineDocument.setDefaultTextOption(m_textEdit->document()->defaultTextOption());
|
||||||
|
singleLineDocument.setPlainText(message);
|
||||||
|
singleLineDocument.setDocumentMargin(m_textEdit->document()->documentMargin());
|
||||||
|
singleLineDocument.setTextWidth(-1);
|
||||||
|
|
||||||
const QRect wrappedRect = metrics.boundingRect(
|
const int singleLineWidth = qCeil(singleLineDocument.idealWidth())
|
||||||
QRect(0, 0, preferredWidth - BubblePaddingWidth, 10000),
|
+ BubblePaddingWidth
|
||||||
Qt::TextWordWrap,
|
+ BubbleWidthSafetyMargin;
|
||||||
message);
|
int preferredWidth = qBound(MinBubbleWidth, singleLineWidth, MaxBubbleWidth);
|
||||||
|
|
||||||
const int preferredHeight = qMin(wrappedRect.height() + BubblePaddingHeight, MaxBubbleHeight);
|
QSizeF documentSize = documentSizeForWidth(m_textEdit, message, preferredWidth - BubblePaddingWidth);
|
||||||
return QSize(preferredWidth, preferredHeight);
|
int preferredHeight = qCeil(documentSize.height()) + BubblePaddingHeight;
|
||||||
|
if (preferredHeight > MaxBubbleHeight)
|
||||||
|
{
|
||||||
|
const int scrollbarWidth = m_textEdit->verticalScrollBar()->sizeHint().width();
|
||||||
|
preferredWidth = qMin(MaxBubbleWidth, preferredWidth + scrollbarWidth);
|
||||||
|
documentSize = documentSizeForWidth(m_textEdit, message, preferredWidth - BubblePaddingWidth - scrollbarWidth);
|
||||||
|
preferredHeight = qCeil(documentSize.height()) + BubblePaddingHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QSize(preferredWidth, qMin(preferredHeight, MaxBubbleHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBubble::updatePosition()
|
void ChatBubble::updatePosition()
|
||||||
|
|||||||
+142
-20
@@ -28,6 +28,7 @@
|
|||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -46,6 +47,20 @@ QString previewImagePath()
|
|||||||
constexpr int MaxUserMessageLength = 4000;
|
constexpr int MaxUserMessageLength = 4000;
|
||||||
constexpr int ChatInputLowerOffsetY = 48;
|
constexpr int ChatInputLowerOffsetY = 48;
|
||||||
constexpr int StreamBubbleUpdateIntervalMs = 80;
|
constexpr int StreamBubbleUpdateIntervalMs = 80;
|
||||||
|
constexpr int BaseAnimationTargetSize = 320;
|
||||||
|
constexpr int LowPowerFpsCap = 6;
|
||||||
|
constexpr int ChatFinishedReturnDelayMs = 1500;
|
||||||
|
|
||||||
|
AppConfig normalizedAppConfig(AppConfig config)
|
||||||
|
{
|
||||||
|
config.scale = qBound(0.5, config.scale, 2.0);
|
||||||
|
if (config.performanceMode != QStringLiteral("standard")
|
||||||
|
&& config.performanceMode != QStringLiteral("low-power"))
|
||||||
|
{
|
||||||
|
config.performanceMode = QStringLiteral("standard");
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
bool populateRuntimeApiKey(AIConfig &config, QString *errorMessage)
|
bool populateRuntimeApiKey(AIConfig &config, QString *errorMessage)
|
||||||
{
|
{
|
||||||
@@ -199,17 +214,41 @@ PetWindow::~PetWindow() = default;
|
|||||||
|
|
||||||
void PetWindow::applyAppConfig(const AppConfig &config)
|
void PetWindow::applyAppConfig(const AppConfig &config)
|
||||||
{
|
{
|
||||||
setAlwaysOnTop(config.alwaysOnTop);
|
const AppConfig normalizedConfig = normalizedAppConfig(config);
|
||||||
|
const bool rebuildClips = !qFuzzyCompare(m_appConfig.scale, normalizedConfig.scale)
|
||||||
|
|| m_appConfig.performanceMode != normalizedConfig.performanceMode
|
||||||
|
|| m_appConfig.enableLazyLoad != normalizedConfig.enableLazyLoad;
|
||||||
|
|
||||||
if (config.hasWindowPosition && isPointVisibleOnScreen(config.windowPosition))
|
m_appConfig = normalizedConfig;
|
||||||
|
setAlwaysOnTop(m_appConfig.alwaysOnTop);
|
||||||
|
|
||||||
|
if (m_appConfig.hasWindowPosition && isPointVisibleOnScreen(m_appConfig.windowPosition))
|
||||||
{
|
{
|
||||||
move(config.windowPosition);
|
move(m_appConfig.windowPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rebuildClips && !m_characterPackage.states.isEmpty())
|
||||||
|
{
|
||||||
|
const QString previousState = m_stateMachine.currentState().isEmpty()
|
||||||
|
? QStringLiteral("idle")
|
||||||
|
: m_stateMachine.currentState();
|
||||||
|
|
||||||
|
m_frameAnimator.stop();
|
||||||
|
buildAnimationClips();
|
||||||
|
|
||||||
|
const QString nextState = m_clips.contains(previousState)
|
||||||
|
? previousState
|
||||||
|
: QStringLiteral("idle");
|
||||||
|
if (m_clips.contains(nextState))
|
||||||
|
{
|
||||||
|
playResolvedState(m_stateMachine.requestState(nextState), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig PetWindow::currentAppConfig() const
|
AppConfig PetWindow::currentAppConfig() const
|
||||||
{
|
{
|
||||||
AppConfig config;
|
AppConfig config = m_appConfig;
|
||||||
config.windowPosition = pos();
|
config.windowPosition = pos();
|
||||||
config.hasWindowPosition = true;
|
config.hasWindowPosition = true;
|
||||||
config.alwaysOnTop = m_alwaysOnTop;
|
config.alwaysOnTop = m_alwaysOnTop;
|
||||||
@@ -218,6 +257,11 @@ AppConfig PetWindow::currentAppConfig() const
|
|||||||
|
|
||||||
void PetWindow::pauseAnimation()
|
void PetWindow::pauseAnimation()
|
||||||
{
|
{
|
||||||
|
if (!m_appConfig.pauseWhenHidden)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_returnToIdleAfterResume = m_behaviorReturnTimer.isActive();
|
m_returnToIdleAfterResume = m_behaviorReturnTimer.isActive();
|
||||||
m_idleBehaviorTimer.stop();
|
m_idleBehaviorTimer.stop();
|
||||||
m_behaviorReturnTimer.stop();
|
m_behaviorReturnTimer.stop();
|
||||||
@@ -294,11 +338,20 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event)
|
|||||||
else if (selectedAction == settingsAction)
|
else if (selectedAction == settingsAction)
|
||||||
{
|
{
|
||||||
ConfigManager configManager;
|
ConfigManager configManager;
|
||||||
SettingsDialog dialog(configManager.loadAIConfigStore(), this);
|
SettingsDialog dialog(configManager.loadAIConfigStore(), currentAppConfig(), this);
|
||||||
if (dialog.exec() == QDialog::Accepted && !configManager.saveAIConfigStore(dialog.aiConfigStore()))
|
if (dialog.exec() == QDialog::Accepted)
|
||||||
|
{
|
||||||
|
if (!configManager.saveAIConfigStore(dialog.aiConfigStore()))
|
||||||
{
|
{
|
||||||
Logger::warning(QStringLiteral("Failed to save AI config from settings dialog."));
|
Logger::warning(QStringLiteral("Failed to save AI config from settings dialog."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyAppConfig(dialog.appConfig());
|
||||||
|
if (!configManager.saveAppConfig(currentAppConfig()))
|
||||||
|
{
|
||||||
|
Logger::warning(QStringLiteral("Failed to save app config from settings dialog."));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (selectedAction == exitAction)
|
else if (selectedAction == exitAction)
|
||||||
{
|
{
|
||||||
@@ -306,6 +359,11 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event)
|
|||||||
}
|
}
|
||||||
else if (selectedAction != nullptr && selectedAction->data().isValid())
|
else if (selectedAction != nullptr && selectedAction->data().isValid())
|
||||||
{
|
{
|
||||||
|
if (isManualStateSwitchLocked())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
playState(selectedAction->data().toString(), false);
|
playState(selectedAction->data().toString(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,10 +448,20 @@ bool PetWindow::submitChatMessage(const QString &message)
|
|||||||
window->m_streamBubbleUpdateTimer.stop();
|
window->m_streamBubbleUpdateTimer.stop();
|
||||||
if (response.success)
|
if (response.success)
|
||||||
{
|
{
|
||||||
|
const bool shouldReturnToIdleAfterChat =
|
||||||
|
window->m_streamingTalkStarted
|
||||||
|
|| window->m_stateMachine.currentState() == QStringLiteral("think")
|
||||||
|
|| window->m_stateMachine.currentState() == QStringLiteral("talk")
|
||||||
|
|| window->m_frameAnimator.currentStateName() == QStringLiteral("think")
|
||||||
|
|| window->m_frameAnimator.currentStateName() == QStringLiteral("talk");
|
||||||
window->finishStreamingChat();
|
window->finishStreamingChat();
|
||||||
window->m_streamingAssistantText = response.content;
|
window->m_streamingAssistantText = response.content;
|
||||||
window->flushStreamingBubble(true);
|
window->flushStreamingBubble(true);
|
||||||
window->refreshChatHistoryPanel();
|
window->refreshChatHistoryPanel();
|
||||||
|
if (shouldReturnToIdleAfterChat)
|
||||||
|
{
|
||||||
|
window->m_behaviorReturnTimer.start(ChatFinishedReturnDelayMs);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +510,17 @@ bool PetWindow::hasActiveAIRequest() const
|
|||||||
return m_conversationManager && m_conversationManager->isBusy();
|
return m_conversationManager && m_conversationManager->isBusy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PetWindow::isManualStateSwitchLocked() const
|
||||||
|
{
|
||||||
|
if (m_streamingChatActive || hasActiveAIRequest())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString currentState = m_stateMachine.currentState();
|
||||||
|
return currentState == QStringLiteral("think") || currentState == QStringLiteral("talk");
|
||||||
|
}
|
||||||
|
|
||||||
void PetWindow::showConversationHistory()
|
void PetWindow::showConversationHistory()
|
||||||
{
|
{
|
||||||
const QVector<ChatMessage> history = m_conversationManager ? m_conversationManager->history() : QVector<ChatMessage>();
|
const QVector<ChatMessage> history = m_conversationManager ? m_conversationManager->history() : QVector<ChatMessage>();
|
||||||
@@ -640,10 +719,12 @@ void PetWindow::buildAnimationClips()
|
|||||||
m_clips.clear();
|
m_clips.clear();
|
||||||
|
|
||||||
QSet<QString> availableStates;
|
QSet<QString> availableStates;
|
||||||
const QSize targetSize(320, 320);
|
const QSize targetSize = animationTargetSize();
|
||||||
|
const bool loadFramesImmediately = !m_appConfig.enableLazyLoad;
|
||||||
for (auto iterator = m_characterPackage.states.constBegin(); iterator != m_characterPackage.states.constEnd(); ++iterator)
|
for (auto iterator = m_characterPackage.states.constBegin(); iterator != m_characterPackage.states.constEnd(); ++iterator)
|
||||||
{
|
{
|
||||||
AnimationClip clip = AnimationClip::fromState(iterator.value(), targetSize);
|
AnimationClip clip = AnimationClip::fromState(iterator.value(), targetSize, loadFramesImmediately);
|
||||||
|
clip.fps = effectiveAnimationFps(clip.fps);
|
||||||
if (!clip.isValid())
|
if (!clip.isValid())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -659,6 +740,7 @@ void PetWindow::buildAnimationClips()
|
|||||||
void PetWindow::addStateTestActions(QMenu *menu)
|
void PetWindow::addStateTestActions(QMenu *menu)
|
||||||
{
|
{
|
||||||
QMenu *stateMenu = menu->addMenu(QStringLiteral("State Test"));
|
QMenu *stateMenu = menu->addMenu(QStringLiteral("State Test"));
|
||||||
|
const bool stateSwitchLocked = isManualStateSwitchLocked();
|
||||||
const QStringList stateNames = {
|
const QStringList stateNames = {
|
||||||
QStringLiteral("idle"),
|
QStringLiteral("idle"),
|
||||||
QStringLiteral("talk"),
|
QStringLiteral("talk"),
|
||||||
@@ -680,7 +762,7 @@ void PetWindow::addStateTestActions(QMenu *menu)
|
|||||||
stateAction->setData(stateName);
|
stateAction->setData(stateName);
|
||||||
}
|
}
|
||||||
|
|
||||||
stateMenu->setEnabled(!stateMenu->actions().isEmpty());
|
stateMenu->setEnabled(!stateSwitchLocked && !stateMenu->actions().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
void PetWindow::updateBubblePosition()
|
void PetWindow::updateBubblePosition()
|
||||||
@@ -709,17 +791,17 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
|
|||||||
&& stateName != QStringLiteral("error")
|
&& stateName != QStringLiteral("error")
|
||||||
&& stateName != QStringLiteral("drag"))
|
&& stateName != QStringLiteral("drag"))
|
||||||
{
|
{
|
||||||
const QString heldState = m_streamingTalkStarted
|
const QString preferredHeldState = m_streamingTalkStarted
|
||||||
? QStringLiteral("talk")
|
? QStringLiteral("talk")
|
||||||
: QStringLiteral("think");
|
: QStringLiteral("think");
|
||||||
|
const QString heldState = m_stateMachine.requestState(preferredHeldState, StateRequestSource::System);
|
||||||
if (stateName != heldState && m_clips.contains(heldState))
|
|
||||||
{
|
|
||||||
playResolvedState(heldState, centerWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateName != heldState)
|
if (stateName != heldState)
|
||||||
{
|
{
|
||||||
|
if (!heldState.isEmpty() && m_clips.contains(heldState))
|
||||||
|
{
|
||||||
|
playResolvedState(heldState, centerWindow);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -729,13 +811,18 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto clipIterator = m_clips.constFind(stateName);
|
auto clipIterator = m_clips.find(stateName);
|
||||||
if (clipIterator == m_clips.constEnd())
|
if (clipIterator == m_clips.end())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationClip *clip = &clipIterator.value();
|
||||||
|
if (!clip->ensureLoaded())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimationClip *clip = &clipIterator.value();
|
|
||||||
m_idleBehaviorTimer.stop();
|
m_idleBehaviorTimer.stop();
|
||||||
m_behaviorReturnTimer.stop();
|
m_behaviorReturnTimer.stop();
|
||||||
m_centerNextFrame = centerWindow;
|
m_centerNextFrame = centerWindow;
|
||||||
@@ -751,6 +838,28 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QSize PetWindow::animationTargetSize() const
|
||||||
|
{
|
||||||
|
const int sideLength = qRound(BaseAnimationTargetSize * m_appConfig.scale);
|
||||||
|
const int boundedSideLength = qBound(BaseAnimationTargetSize / 2, sideLength, BaseAnimationTargetSize * 2);
|
||||||
|
return QSize(boundedSideLength, boundedSideLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
int PetWindow::effectiveAnimationFps(int fps) const
|
||||||
|
{
|
||||||
|
if (isLowPowerMode())
|
||||||
|
{
|
||||||
|
return qMax(1, qMin(fps, LowPowerFpsCap));
|
||||||
|
}
|
||||||
|
|
||||||
|
return qMax(1, fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PetWindow::isLowPowerMode() const
|
||||||
|
{
|
||||||
|
return m_appConfig.performanceMode == QStringLiteral("low-power");
|
||||||
|
}
|
||||||
|
|
||||||
void PetWindow::scheduleIdleBehavior()
|
void PetWindow::scheduleIdleBehavior()
|
||||||
{
|
{
|
||||||
if (!m_clips.contains(QStringLiteral("idle")))
|
if (!m_clips.contains(QStringLiteral("idle")))
|
||||||
@@ -758,7 +867,9 @@ void PetWindow::scheduleIdleBehavior()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int idleDelayMs = QRandomGenerator::global()->bounded(8000, 16001);
|
const int idleDelayMs = isLowPowerMode()
|
||||||
|
? QRandomGenerator::global()->bounded(16000, 30001)
|
||||||
|
: QRandomGenerator::global()->bounded(8000, 16001);
|
||||||
m_idleBehaviorTimer.start(idleDelayMs);
|
m_idleBehaviorTimer.start(idleDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,15 +937,20 @@ void PetWindow::setDisplayImage(const QString &imagePath, bool centerWindow)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QSize targetSize(320, 320);
|
const QSize targetSize = animationTargetSize();
|
||||||
const QPixmap scaled = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
const QPixmap scaled = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
setDisplayPixmap(scaled, centerWindow);
|
setDisplayPixmap(scaled, centerWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PetWindow::setDisplayPixmap(const QPixmap &pixmap, bool centerWindow)
|
void PetWindow::setDisplayPixmap(const QPixmap &pixmap, bool centerWindow)
|
||||||
{
|
{
|
||||||
|
const QSize previousSize = size();
|
||||||
m_petView->setFrame(pixmap);
|
m_petView->setFrame(pixmap);
|
||||||
resize(pixmap.size());
|
resize(pixmap.size());
|
||||||
|
if (size() != previousSize)
|
||||||
|
{
|
||||||
|
updateBubblePosition();
|
||||||
|
}
|
||||||
|
|
||||||
if (centerWindow)
|
if (centerWindow)
|
||||||
{
|
{
|
||||||
@@ -862,6 +978,12 @@ bool PetWindow::isPointVisibleOnScreen(const QPoint &point) const
|
|||||||
|
|
||||||
void PetWindow::setAlwaysOnTop(bool enabled)
|
void PetWindow::setAlwaysOnTop(bool enabled)
|
||||||
{
|
{
|
||||||
|
const bool currentlyOnTop = windowFlags().testFlag(Qt::WindowStaysOnTopHint);
|
||||||
|
if (m_alwaysOnTop == enabled && currentlyOnTop == enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_alwaysOnTop = enabled;
|
m_alwaysOnTop = enabled;
|
||||||
const bool wasVisible = isVisible();
|
const bool wasVisible = isVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -59,12 +59,16 @@ private:
|
|||||||
void finishStreamingChat();
|
void finishStreamingChat();
|
||||||
void cancelStreamingChat();
|
void cancelStreamingChat();
|
||||||
bool hasActiveAIRequest() const;
|
bool hasActiveAIRequest() const;
|
||||||
|
bool isManualStateSwitchLocked() const;
|
||||||
void resetBubbleAutoHideTimer();
|
void resetBubbleAutoHideTimer();
|
||||||
QPoint chatInputAnchorPosition() const;
|
QPoint chatInputAnchorPosition() const;
|
||||||
void updateBubblePosition();
|
void updateBubblePosition();
|
||||||
QPoint bubbleAnchorPosition() const;
|
QPoint bubbleAnchorPosition() const;
|
||||||
void playState(const QString &stateName, bool centerWindow);
|
void playState(const QString &stateName, bool centerWindow);
|
||||||
void playResolvedState(const QString &stateName, bool centerWindow);
|
void playResolvedState(const QString &stateName, bool centerWindow);
|
||||||
|
QSize animationTargetSize() const;
|
||||||
|
int effectiveAnimationFps(int fps) const;
|
||||||
|
bool isLowPowerMode() const;
|
||||||
void scheduleIdleBehavior();
|
void scheduleIdleBehavior();
|
||||||
void playIdleBehavior();
|
void playIdleBehavior();
|
||||||
void returnToIdleFromBehavior();
|
void returnToIdleFromBehavior();
|
||||||
@@ -81,6 +85,7 @@ private:
|
|||||||
QTimer m_idleBehaviorTimer;
|
QTimer m_idleBehaviorTimer;
|
||||||
QTimer m_behaviorReturnTimer;
|
QTimer m_behaviorReturnTimer;
|
||||||
QTimer m_streamBubbleUpdateTimer;
|
QTimer m_streamBubbleUpdateTimer;
|
||||||
|
AppConfig m_appConfig;
|
||||||
CharacterPackage m_characterPackage;
|
CharacterPackage m_characterPackage;
|
||||||
QMap<QString, AnimationClip> m_clips;
|
QMap<QString, AnimationClip> m_clips;
|
||||||
FrameAnimator m_frameAnimator;
|
FrameAnimator m_frameAnimator;
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
|
#include <QTabWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -28,7 +31,7 @@ QString normalizedProviderName(const QString &provider)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent)
|
SettingsDialog::SettingsDialog(const AIConfigStore &configStore, const AppConfig &appConfig, QWidget *parent)
|
||||||
: QDialog(parent)
|
: QDialog(parent)
|
||||||
, m_providerComboBox(new QComboBox(this))
|
, m_providerComboBox(new QComboBox(this))
|
||||||
, m_baseUrlEdit(new QLineEdit(this))
|
, m_baseUrlEdit(new QLineEdit(this))
|
||||||
@@ -39,11 +42,16 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
|
|||||||
, m_temperatureSpinBox(new QDoubleSpinBox(this))
|
, m_temperatureSpinBox(new QDoubleSpinBox(this))
|
||||||
, m_maxTokensSpinBox(new QSpinBox(this))
|
, m_maxTokensSpinBox(new QSpinBox(this))
|
||||||
, m_allowPlainApiKeyCheckBox(new QCheckBox(QStringLiteral("允许在非 Windows 环境明文保存 API Key"), this))
|
, m_allowPlainApiKeyCheckBox(new QCheckBox(QStringLiteral("允许在非 Windows 环境明文保存 API Key"), this))
|
||||||
|
, m_scaleSpinBox(new QSpinBox(this))
|
||||||
|
, m_performanceModeComboBox(new QComboBox(this))
|
||||||
|
, m_pauseWhenHiddenCheckBox(new QCheckBox(QStringLiteral("隐藏到托盘时暂停动画"), this))
|
||||||
|
, m_enableLazyLoadCheckBox(new QCheckBox(QStringLiteral("启用动画懒加载"), this))
|
||||||
, m_configStore(configStore)
|
, m_configStore(configStore)
|
||||||
|
, m_appConfig(appConfig)
|
||||||
{
|
{
|
||||||
setWindowTitle(QStringLiteral("设置"));
|
setWindowTitle(QStringLiteral("设置"));
|
||||||
setModal(true);
|
setModal(true);
|
||||||
resize(460, 360);
|
resize(500, 430);
|
||||||
|
|
||||||
const QList<QPair<QString, QString>> providers = {
|
const QList<QPair<QString, QString>> providers = {
|
||||||
{QStringLiteral("openai"), QStringLiteral("OpenAI")},
|
{QStringLiteral("openai"), QStringLiteral("OpenAI")},
|
||||||
@@ -72,6 +80,19 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
|
|||||||
|
|
||||||
m_maxTokensSpinBox->setRange(1, 200000);
|
m_maxTokensSpinBox->setRange(1, 200000);
|
||||||
|
|
||||||
|
m_scaleSpinBox->setRange(50, 200);
|
||||||
|
m_scaleSpinBox->setSingleStep(10);
|
||||||
|
m_scaleSpinBox->setSuffix(QStringLiteral("%"));
|
||||||
|
m_scaleSpinBox->setValue(qRound(qBound(0.5, m_appConfig.scale, 2.0) * 100.0));
|
||||||
|
|
||||||
|
m_performanceModeComboBox->addItem(QStringLiteral("标准"), QStringLiteral("standard"));
|
||||||
|
m_performanceModeComboBox->addItem(QStringLiteral("低功耗"), QStringLiteral("low-power"));
|
||||||
|
const int performanceModeIndex = m_performanceModeComboBox->findData(m_appConfig.performanceMode);
|
||||||
|
m_performanceModeComboBox->setCurrentIndex(performanceModeIndex >= 0 ? performanceModeIndex : 0);
|
||||||
|
|
||||||
|
m_pauseWhenHiddenCheckBox->setChecked(m_appConfig.pauseWhenHidden);
|
||||||
|
m_enableLazyLoadCheckBox->setChecked(m_appConfig.enableLazyLoad);
|
||||||
|
|
||||||
m_allowPlainApiKeyCheckBox->setVisible(!SecretStore::isEncryptionAvailable());
|
m_allowPlainApiKeyCheckBox->setVisible(!SecretStore::isEncryptionAvailable());
|
||||||
m_currentProvider = normalizedProviderName(m_providerComboBox->currentData().toString());
|
m_currentProvider = normalizedProviderName(m_providerComboBox->currentData().toString());
|
||||||
loadProviderConfig(m_currentProvider);
|
loadProviderConfig(m_currentProvider);
|
||||||
@@ -87,6 +108,22 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
|
|||||||
formLayout->addRow(QStringLiteral("Max Tokens"), m_maxTokensSpinBox);
|
formLayout->addRow(QStringLiteral("Max Tokens"), m_maxTokensSpinBox);
|
||||||
formLayout->addRow(QString(), m_allowPlainApiKeyCheckBox);
|
formLayout->addRow(QString(), m_allowPlainApiKeyCheckBox);
|
||||||
|
|
||||||
|
auto *aiPage = new QWidget(this);
|
||||||
|
aiPage->setLayout(formLayout);
|
||||||
|
|
||||||
|
auto *appFormLayout = new QFormLayout();
|
||||||
|
appFormLayout->addRow(QStringLiteral("缩放"), m_scaleSpinBox);
|
||||||
|
appFormLayout->addRow(QStringLiteral("性能模式"), m_performanceModeComboBox);
|
||||||
|
appFormLayout->addRow(QString(), m_pauseWhenHiddenCheckBox);
|
||||||
|
appFormLayout->addRow(QString(), m_enableLazyLoadCheckBox);
|
||||||
|
|
||||||
|
auto *appPage = new QWidget(this);
|
||||||
|
appPage->setLayout(appFormLayout);
|
||||||
|
|
||||||
|
auto *tabWidget = new QTabWidget(this);
|
||||||
|
tabWidget->addTab(aiPage, QStringLiteral("AI"));
|
||||||
|
tabWidget->addTab(appPage, QStringLiteral("应用"));
|
||||||
|
|
||||||
auto *storageHintLabel = new QLabel(this);
|
auto *storageHintLabel = new QLabel(this);
|
||||||
storageHintLabel->setWordWrap(true);
|
storageHintLabel->setWordWrap(true);
|
||||||
storageHintLabel->setText(SecretStore::isEncryptionAvailable()
|
storageHintLabel->setText(SecretStore::isEncryptionAvailable()
|
||||||
@@ -98,7 +135,7 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
|
|||||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
auto *layout = new QVBoxLayout(this);
|
||||||
layout->addLayout(formLayout);
|
layout->addWidget(tabWidget);
|
||||||
layout->addWidget(storageHintLabel);
|
layout->addWidget(storageHintLabel);
|
||||||
layout->addWidget(buttonBox);
|
layout->addWidget(buttonBox);
|
||||||
|
|
||||||
@@ -118,6 +155,20 @@ AIConfigStore SettingsDialog::aiConfigStore() const
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppConfig SettingsDialog::appConfig() const
|
||||||
|
{
|
||||||
|
AppConfig config = m_appConfig;
|
||||||
|
config.scale = m_scaleSpinBox->value() / 100.0;
|
||||||
|
config.performanceMode = m_performanceModeComboBox->currentData().toString();
|
||||||
|
if (config.performanceMode.isEmpty())
|
||||||
|
{
|
||||||
|
config.performanceMode = QStringLiteral("standard");
|
||||||
|
}
|
||||||
|
config.pauseWhenHidden = m_pauseWhenHiddenCheckBox->isChecked();
|
||||||
|
config.enableLazyLoad = m_enableLazyLoadCheckBox->isChecked();
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsDialog::cacheCurrentProvider()
|
void SettingsDialog::cacheCurrentProvider()
|
||||||
{
|
{
|
||||||
if (m_currentProvider.isEmpty())
|
if (m_currentProvider.isEmpty())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../config/AIConfig.h"
|
#include "../config/AIConfig.h"
|
||||||
|
#include "../config/AppConfig.h"
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
|
||||||
@@ -13,9 +14,10 @@ class QSpinBox;
|
|||||||
class SettingsDialog : public QDialog
|
class SettingsDialog : public QDialog
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit SettingsDialog(const AIConfigStore &configStore, QWidget *parent = nullptr);
|
explicit SettingsDialog(const AIConfigStore &configStore, const AppConfig &appConfig, QWidget *parent = nullptr);
|
||||||
|
|
||||||
AIConfigStore aiConfigStore() const;
|
AIConfigStore aiConfigStore() const;
|
||||||
|
AppConfig appConfig() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void cacheCurrentProvider();
|
void cacheCurrentProvider();
|
||||||
@@ -33,6 +35,11 @@ private:
|
|||||||
QDoubleSpinBox *m_temperatureSpinBox = nullptr;
|
QDoubleSpinBox *m_temperatureSpinBox = nullptr;
|
||||||
QSpinBox *m_maxTokensSpinBox = nullptr;
|
QSpinBox *m_maxTokensSpinBox = nullptr;
|
||||||
QCheckBox *m_allowPlainApiKeyCheckBox = nullptr;
|
QCheckBox *m_allowPlainApiKeyCheckBox = nullptr;
|
||||||
|
QSpinBox *m_scaleSpinBox = nullptr;
|
||||||
|
QComboBox *m_performanceModeComboBox = nullptr;
|
||||||
|
QCheckBox *m_pauseWhenHiddenCheckBox = nullptr;
|
||||||
|
QCheckBox *m_enableLazyLoadCheckBox = nullptr;
|
||||||
AIConfigStore m_configStore;
|
AIConfigStore m_configStore;
|
||||||
|
AppConfig m_appConfig;
|
||||||
QString m_currentProvider;
|
QString m_currentProvider;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user