diff --git a/docs/prompt_quality_module_plan.md b/docs/prompt_quality_module_plan.md new file mode 100644 index 0000000..da009b9 --- /dev/null +++ b/docs/prompt_quality_module_plan.md @@ -0,0 +1,117 @@ +# 提示词质量对比与评价模块 — 开发计划 + +| 项 | 说明 | +|----|------| +| 目标 | 多份优化后提示词 → 模型结构化评价;评价历史检索与简单可视化对比 | +| 存储 | 与现有 Flask-SQLAlchemy 一致;开发默认 SQLite(`DATABASE_URL` 未设时);生产可用 MySQL,表结构通用 | + +## 1. 接口定义 + +### POST `/api/prompt-quality/analyze` + +**输入** `application/json`: + +```json +{ + "prompts": ["提示词文本1", "提示词文本2"], + "task_context": "可选:任务背景/领域,便于模型对齐预期输出" +} +``` + +**输出** `application/json`: + +```json +{ + "success": true, + "data": { + "batch_id": "uuid", + "results": [ + { + "optimized_prompt": "与输入对应的完整文本", + "evaluation": { + "score": 82, + "strengths": ["..."], + "weaknesses": ["..."], + "suggestions": ["..."], + "applicability": ["..."], + "mandatory_fixes": ["必须修正项"], + "optimization_suggestions": ["可优化建议"] + }, + "history_id": "记录主键字符串", + "timestamp": "2026-04-03T12:00:00+00:00" + } + ], + "batch_comparison": "批量横向对比摘要(可选)", + "incremental_learning_hints": ["从本批提炼的可迁移经验"] + } +} +``` + +### GET `/api/prompt-quality/history` + +**Query**:`page`, `per_page`, `date_from`, `date_to`(ISO 日期), `q`(关键词,匹配提示词片段或评价 JSON 文本), `batch_id`(可选) + +**输出**:分页列表,每条含 `batch_id`、`created_at`、`preview`、`item_count`、`results` 摘要或详情开关。 + +### GET `/api/prompt-quality/history/` + +单条记录详情(权限:同 `user_id`)。 + +## 2. 评价逻辑(算法描述) + +1. **预处理**:去空、单条最大长度截断(如 8000 字符)、条数上限(如 ≤8)。 +2. **模型调用**:系统角色 = 提示词评估专家;用户内容 = 编号列表 + 可选 `task_context`;要求 **仅输出合法 JSON**(与约定 schema 一致)。 +3. **维度约束**(写入 system 说明):清晰度、指令完整性、可操作性、预期输出匹配度;区分 **mandatory_fixes**(必须修正)与 **optimization_suggestions**(建议优化)。 +4. **校验**:`json.loads`;缺字段时用安全默认值补全;`score` 钳制 0–100。 +5. **持久化**:每条输入写一行 `prompt_quality_record`;同批次共用 `batch_id`;批次级 `batch_comparison` / `incremental_learning_hints` 挂在 `prompt_index=0` 行或独立字段。 + +## 3. 表结构要点 + +**`prompt_quality_record`** + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | PK | `history_id` 来源 | +| user_id | int, index | 与 Session 对齐 | +| batch_id | string(36), index | 同次分析 UUID | +| prompt_index | int | 批次内序号 | +| optimized_prompt | text | 被评文本 | +| evaluation_json | JSON | 结构化评价(含两类优先级字段) | +| batch_summary | text, nullable | 批次对比摘要(仅首条或冗余存储) | +| incremental_hints_json | JSON, nullable | 增量学习建议列表 | +| created_at | datetime | ISO 时间来源 | + +SQLite:`JSON` 由 SQLAlchemy 映射为 TEXT;MySQL 可用原生 JSON。 + +## 4. 查询 API 示例 + +```http +GET /api/prompt-quality/history?page=1&per_page=10&date_from=2026-04-01&date_to=2026-04-03&q=openlaw +``` + +## 5. 前端(Vue) + +- 路由:`/prompt-quality` +- 多行文本输入,约定 **空行分隔** 多条提示词;按钮「模型分析评价」 +- 展示:卡片列表(score、优劣、两类建议、适用场景);批次对比与增量建议 +- 历史区:表格 + 筛选 + 查看详情抽屉 + +--- + +**实施顺序**:模型与建表 SQL → service + 蓝图 → 注册应用 → Vue API + 页面 + 路由 + 导航。 + +--- + +## 实施记录(已完成) + +| 组件 | 路径 | +|------|------| +| 计划文档 | `docs/prompt_quality_module_plan.md` | +| ORM | `src/flask_prompt_master/models/prompt_quality_models.py` | +| LLM 服务 | `src/flask_prompt_master/services/prompt_quality_service.py` | +| API 蓝图 | `src/flask_prompt_master/routes/prompt_quality_routes.py`,前缀 `/api/prompt-quality` | +| 应用注册 | `src/flask_prompt_master/__init__.py` 注册蓝图并加载模型 | +| 手工建表 SQL(SQLite 友好) | `scripts/prompt_quality_schema.sql` | +| 建表 | 首次可执行 `db.create_all()`(已在当前环境对 MySQL 验证) | +| Vue 页面 | `vue-app/src/views/PromptQualityView.vue`,路由 `/prompt-quality`,顶栏「质量评价」 | +| Vue API | `vue-app/src/api/modules/promptQuality.ts`、`api/types/promptQuality.ts` | diff --git a/scripts/ensure_evaluation_schema_mysql.sql b/scripts/ensure_evaluation_schema_mysql.sql new file mode 100644 index 0000000..3f3db43 --- /dev/null +++ b/scripts/ensure_evaluation_schema_mysql.sql @@ -0,0 +1,48 @@ +-- 评价 / 对比功能所需结构(在已有库上增量执行;若列或表已存在会报错,可忽略对应语句) +-- MySQL 5.7+ / 8.0 + +-- 1. prompt_history 扩展字段(模型 history_models.PromptHistory 依赖) +-- 分两条执行:避免 AFTER 依赖列名/顺序;若某列已存在会报 Duplicate column,可忽略该条 +ALTER TABLE prompt_history + ADD COLUMN comparison_group_id VARCHAR(64) NULL COMMENT '对比组ID'; +ALTER TABLE prompt_history + ADD COLUMN is_comparison_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与对比'; +ALTER TABLE prompt_history ADD INDEX idx_comparison_group (comparison_group_id); + +-- 若上面因「列已存在」失败,说明已执行过,可继续执行下面建表。 + +-- 2. 评价表(与 evaluation_models.PromptEvaluation 一致) +CREATE TABLE IF NOT EXISTS prompt_evaluation ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + history_id INT NOT NULL COMMENT '历史记录ID', + user_id INT NOT NULL COMMENT '评价者用户ID', + clarity_rating TINYINT NULL COMMENT '清晰度评分', + specificity_rating TINYINT NULL COMMENT '具体性评分', + effectiveness_rating TINYINT NULL COMMENT '有效性评分', + professionalism_rating TINYINT NULL COMMENT '专业性评分', + completeness_rating TINYINT NULL COMMENT '完整性评分', + is_best_version TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为最佳版本', + comparison_group_id VARCHAR(64) NULL COMMENT '对比组ID', + overall_rating TINYINT NULL COMMENT '综合评分', + comments TEXT NULL COMMENT '评价意见', + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_history_id (history_id), + INDEX idx_user_id (user_id), + INDEX idx_comparison_group (comparison_group_id), + CONSTRAINT fk_pe_history FOREIGN KEY (history_id) REFERENCES prompt_history(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提示词评价表'; + +-- 3. 对比组表 +CREATE TABLE IF NOT EXISTS comparison_group ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + group_id VARCHAR(64) NOT NULL COMMENT '对比组ID', + user_id INT NOT NULL COMMENT '创建者用户ID', + name VARCHAR(100) NULL COMMENT '对比组名称', + description TEXT NULL COMMENT '对比组描述', + is_public TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否公开', + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE KEY uk_group_id (group_id), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对比组表'; diff --git a/scripts/ensure_evaluation_schema_sqlite.sql b/scripts/ensure_evaluation_schema_sqlite.sql new file mode 100644 index 0000000..ff527b3 --- /dev/null +++ b/scripts/ensure_evaluation_schema_sqlite.sql @@ -0,0 +1,39 @@ +-- 开发环境默认 SQLite(config/development.py → sqlite:///dev.db)增量修复 +-- 在仓库根目录执行: sqlite3 dev.db < scripts/ensure_evaluation_schema_sqlite.sql +-- 若列已存在会报错,可忽略该句或先 .schema prompt_history 查看 + +ALTER TABLE prompt_history ADD COLUMN comparison_group_id VARCHAR(64); +ALTER TABLE prompt_history ADD COLUMN is_comparison_enabled INTEGER NOT NULL DEFAULT 0; + +-- 评价表(与 ORM 一致;若已存在可跳过整段) +CREATE TABLE IF NOT EXISTS prompt_evaluation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + history_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + clarity_rating INTEGER, + specificity_rating INTEGER, + effectiveness_rating INTEGER, + professionalism_rating INTEGER, + completeness_rating INTEGER, + is_best_version INTEGER NOT NULL DEFAULT 0, + comparison_group_id VARCHAR(64), + overall_rating INTEGER, + comments TEXT, + created_at TIMESTAMP, + updated_at TIMESTAMP, + FOREIGN KEY (history_id) REFERENCES prompt_history(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS comparison_group ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id VARCHAR(64) NOT NULL UNIQUE, + user_id INTEGER NOT NULL, + name VARCHAR(100), + description TEXT, + is_public INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP, + updated_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_pe_history ON prompt_evaluation(history_id); +CREATE INDEX IF NOT EXISTS idx_pe_user ON prompt_evaluation(user_id); diff --git a/scripts/prompt_quality_schema.sql b/scripts/prompt_quality_schema.sql new file mode 100644 index 0000000..c62e8d4 --- /dev/null +++ b/scripts/prompt_quality_schema.sql @@ -0,0 +1,15 @@ +-- 提示词结构化质量评价表(SQLite 与 MySQL 通用写法:MySQL 可将 evaluation_json 改为 JSON 类型) +CREATE TABLE IF NOT EXISTS prompt_quality_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + batch_id VARCHAR(36) NOT NULL, + prompt_index INTEGER NOT NULL DEFAULT 0, + optimized_prompt TEXT NOT NULL, + evaluation_json TEXT NOT NULL, + batch_summary TEXT, + incremental_hints_json TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS ix_pqr_user_id ON prompt_quality_record (user_id); +CREATE INDEX IF NOT EXISTS ix_pqr_batch_id ON prompt_quality_record (batch_id); +CREATE INDEX IF NOT EXISTS ix_pqr_created_at ON prompt_quality_record (created_at); diff --git a/scripts/run_prompt_history_mysql_migration.py b/scripts/run_prompt_history_mysql_migration.py new file mode 100644 index 0000000..1837d21 --- /dev/null +++ b/scripts/run_prompt_history_mysql_migration.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""为 prompt_history 增加对比相关列(可重复执行,已存在则跳过)。""" +from __future__ import annotations + +import os +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from dotenv import load_dotenv +from sqlalchemy import create_engine, text + +load_dotenv(ROOT / ".env") + +DATABASE_URL = os.environ.get("DATABASE_URL") +if not DATABASE_URL: + print("ERROR: 未设置 DATABASE_URL", file=sys.stderr) + sys.exit(1) + +STATEMENTS = [ + ( + "comparison_group_id", + "ALTER TABLE prompt_history ADD COLUMN comparison_group_id VARCHAR(64) NULL COMMENT '对比组ID'", + ), + ( + "is_comparison_enabled", + "ALTER TABLE prompt_history ADD COLUMN is_comparison_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与对比'", + ), + ( + "idx_comparison_group", + "ALTER TABLE prompt_history ADD INDEX idx_comparison_group (comparison_group_id)", + ), +] + + +def main() -> int: + engine = create_engine(DATABASE_URL) + ok = 0 + with engine.connect() as conn: + for name, sql in STATEMENTS: + try: + conn.execute(text(sql)) + conn.commit() + print(f"OK: {name}") + ok += 1 + except Exception as e: + conn.rollback() + err = str(e).lower() + if "duplicate" in err or "1060" in str(e) or "1061" in str(e) or "1826" in str(e): + print(f"SKIP (已存在): {name}") + else: + print(f"FAIL: {name} -> {e}", file=sys.stderr) + return 1 + print(f"完成,成功执行 {ok} 条新变更。") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/flask_prompt_master/__init__.py b/src/flask_prompt_master/__init__.py index 94b3ed7..15ee69e 100644 --- a/src/flask_prompt_master/__init__.py +++ b/src/flask_prompt_master/__init__.py @@ -70,6 +70,11 @@ def create_app(config_class=None): # 注册历史记录蓝图 from src.flask_prompt_master.routes.history_routes import history_bp app.register_blueprint(history_bp) + + # 提示词结构化质量评价(多段文本 + 模型 JSON 评价 + 历史) + from src.flask_prompt_master.models import prompt_quality_models # noqa: F401 + from src.flask_prompt_master.routes.prompt_quality_routes import prompt_quality_bp + app.register_blueprint(prompt_quality_bp) # 注册智能周报生成蓝图 from src.flask_prompt_master.routes.weekly_report import weekly_report_bp diff --git a/src/flask_prompt_master/models/evaluation_models.py b/src/flask_prompt_master/models/evaluation_models.py new file mode 100644 index 0000000..aae575a --- /dev/null +++ b/src/flask_prompt_master/models/evaluation_models.py @@ -0,0 +1,292 @@ +""" +提示词评价数据模型 +支持多维度评价和对比功能 +""" + +from datetime import datetime +from src.flask_prompt_master import db +from sqlalchemy import func, desc, asc, and_, or_ + + +class PromptEvaluation(db.Model): + """提示词评价模型""" + __tablename__ = 'prompt_evaluation' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='主键ID') + history_id = db.Column(db.Integer, db.ForeignKey('prompt_history.id'), nullable=False, comment='历史记录ID') + user_id = db.Column(db.Integer, nullable=False, comment='评价者用户ID') + + # 多维度评分 (1-5分) + clarity_rating = db.Column(db.SmallInteger, comment='清晰度评分') # 是否表达清晰 + specificity_rating = db.Column(db.SmallInteger, comment='具体性评分') # 是否具体明确 + effectiveness_rating = db.Column(db.SmallInteger, comment='有效性评分') # 是否能获得良好响应 + professionalism_rating = db.Column(db.SmallInteger, comment='专业性评分') # 是否专业规范 + completeness_rating = db.Column(db.SmallInteger, comment='完整性评分') # 是否包含必要要素 + + # 对比评价 + is_best_version = db.Column(db.Boolean, default=False, comment='是否为最佳版本') + comparison_group_id = db.Column(db.String(64), comment='对比组ID') # 同一组对比的标识 + + # 综合评价 + overall_rating = db.Column(db.SmallInteger, comment='综合评分') + comments = db.Column(db.Text, comment='评价意见') + created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='创建时间') + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新时间') + + # 关联关系 + history = db.relationship('PromptHistory', backref='evaluations', lazy='select') + + def __repr__(self): + return f'' + + def to_dict(self): + """转换为字典格式""" + return { + 'id': self.id, + 'history_id': self.history_id, + 'user_id': self.user_id, + 'clarity_rating': self.clarity_rating, + 'specificity_rating': self.specificity_rating, + 'effectiveness_rating': self.effectiveness_rating, + 'professionalism_rating': self.professionalism_rating, + 'completeness_rating': self.completeness_rating, + 'is_best_version': self.is_best_version, + 'comparison_group_id': self.comparison_group_id, + 'overall_rating': self.overall_rating, + 'comments': self.comments, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, + # 计算平均维度评分 + 'average_dimension_rating': self.calculate_average_dimension_rating() + } + + def calculate_average_dimension_rating(self): + """计算维度平均评分""" + ratings = [ + self.clarity_rating, + self.specificity_rating, + self.effectiveness_rating, + self.professionalism_rating, + self.completeness_rating + ] + valid_ratings = [r for r in ratings if r is not None] + if not valid_ratings: + return None + return sum(valid_ratings) / len(valid_ratings) + + @classmethod + def submit_evaluation(cls, history_id, user_id, evaluation_data): + """提交评价""" + # 检查是否已存在评价 + existing_evaluation = cls.query.filter_by( + history_id=history_id, + user_id=user_id + ).first() + + if existing_evaluation: + # 更新现有评价 + for key, value in evaluation_data.items(): + if hasattr(existing_evaluation, key): + setattr(existing_evaluation, key, value) + existing_evaluation.updated_at = datetime.utcnow() + db.session.commit() + return existing_evaluation + else: + # 创建新评价 + evaluation = cls( + history_id=history_id, + user_id=user_id, + **evaluation_data + ) + db.session.add(evaluation) + db.session.commit() + return evaluation + + @classmethod + def get_history_evaluations(cls, history_id): + """获取历史记录的所有评价""" + evaluations = cls.query.filter_by(history_id=history_id).all() + return [e.to_dict() for e in evaluations] + + @classmethod + def get_comparison_group_evaluations(cls, comparison_group_id): + """获取对比组的所有评价""" + evaluations = cls.query.filter_by(comparison_group_id=comparison_group_id).all() + return [e.to_dict() for e in evaluations] + + @classmethod + def get_best_version_in_group(cls, comparison_group_id): + """获取对比组中的最佳版本""" + best_evaluation = cls.query.filter_by( + comparison_group_id=comparison_group_id, + is_best_version=True + ).first() + return best_evaluation.to_dict() if best_evaluation else None + + @classmethod + def get_evaluation_statistics(cls, history_id=None, user_id=None, date_from=None, date_to=None): + """获取评价统计信息""" + query = cls.query + + if history_id: + query = query.filter_by(history_id=history_id) + if user_id: + query = query.filter_by(user_id=user_id) + if date_from: + query = query.filter(cls.created_at >= date_from) + if date_to: + query = query.filter(cls.created_at <= date_to) + + # 获取所有评价 + evaluations = query.all() + + if not evaluations: + return { + 'total_evaluations': 0, + 'average_overall_rating': 0, + 'dimension_averages': { + 'clarity': 0, + 'specificity': 0, + 'effectiveness': 0, + 'professionalism': 0, + 'completeness': 0 + }, + 'best_version_count': 0 + } + + # 计算统计信息 + total_evaluations = len(evaluations) + + # 计算综合评分平均值 + overall_ratings = [e.overall_rating for e in evaluations if e.overall_rating is not None] + average_overall_rating = sum(overall_ratings) / len(overall_ratings) if overall_ratings else 0 + + # 计算各维度平均值 + dimension_ratings = { + 'clarity': [], + 'specificity': [], + 'effectiveness': [], + 'professionalism': [], + 'completeness': [] + } + + for row in evaluations: + if row.clarity_rating is not None: + dimension_ratings['clarity'].append(row.clarity_rating) + if row.specificity_rating is not None: + dimension_ratings['specificity'].append(row.specificity_rating) + if row.effectiveness_rating is not None: + dimension_ratings['effectiveness'].append(row.effectiveness_rating) + if row.professionalism_rating is not None: + dimension_ratings['professionalism'].append(row.professionalism_rating) + if row.completeness_rating is not None: + dimension_ratings['completeness'].append(row.completeness_rating) + + dimension_averages = {} + for dimension, ratings in dimension_ratings.items(): + dimension_averages[dimension] = sum(ratings) / len(ratings) if ratings else 0 + + # 计算最佳版本数量 + best_version_count = sum(1 for e in evaluations if e.is_best_version) + + return { + 'total_evaluations': total_evaluations, + 'average_overall_rating': average_overall_rating, + 'dimension_averages': dimension_averages, + 'best_version_count': best_version_count + } + + +class ComparisonGroup(db.Model): + """对比组模型""" + __tablename__ = 'comparison_group' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='主键ID') + group_id = db.Column(db.String(64), unique=True, nullable=False, comment='对比组ID') + user_id = db.Column(db.Integer, nullable=False, comment='创建者用户ID') + name = db.Column(db.String(100), comment='对比组名称') + description = db.Column(db.Text, comment='对比组描述') + is_public = db.Column(db.Boolean, default=False, comment='是否公开') + created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='创建时间') + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新时间') + + def __repr__(self): + return f'' + + def to_dict(self): + """转换为字典格式""" + return { + 'id': self.id, + 'group_id': self.group_id, + 'user_id': self.user_id, + 'name': self.name, + 'description': self.description, + 'is_public': self.is_public, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, + 'history_count': self.get_history_count() + } + + def get_history_count(self): + """获取对比组中的历史记录数量""" + from .history_models import PromptHistory + return PromptHistory.query.filter_by(comparison_group_id=self.group_id).count() + + @classmethod + def create_group(cls, user_id, group_id=None, name=None, description=None, is_public=False): + """创建对比组""" + import uuid + + if not group_id: + group_id = str(uuid.uuid4()) + + group = cls( + group_id=group_id, + user_id=user_id, + name=name, + description=description, + is_public=is_public + ) + db.session.add(group) + db.session.commit() + return group + + @classmethod + def get_user_groups(cls, user_id): + """获取用户的对比组列表""" + groups = cls.query.filter_by(user_id=user_id).all() + return [g.to_dict() for g in groups] + + @classmethod + def get_group_by_id(cls, group_id): + """根据组ID获取对比组""" + group = cls.query.filter_by(group_id=group_id).first() + return group.to_dict() if group else None + + @classmethod + def add_history_to_group(cls, group_id, history_id): + """向对比组添加历史记录""" + from .history_models import PromptHistory + + history = PromptHistory.query.get(history_id) + if not history: + return False + + history.comparison_group_id = group_id + db.session.commit() + return True + + @classmethod + def remove_history_from_group(cls, group_id, history_id): + """从对比组移除历史记录""" + from .history_models import PromptHistory + + history = PromptHistory.query.get(history_id) + if not history: + return False + + if history.comparison_group_id == group_id: + history.comparison_group_id = None + db.session.commit() + return True + return False \ No newline at end of file diff --git a/src/flask_prompt_master/models/prompt_quality_models.py b/src/flask_prompt_master/models/prompt_quality_models.py new file mode 100644 index 0000000..9daaba8 --- /dev/null +++ b/src/flask_prompt_master/models/prompt_quality_models.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +"""提示词结构化质量评价记录(与优化历史 prompt_history 独立)""" + +from datetime import datetime +from src.flask_prompt_master import db + + +class PromptQualityRecord(db.Model): + __tablename__ = 'prompt_quality_record' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + user_id = db.Column(db.Integer, nullable=False, index=True) + batch_id = db.Column(db.String(36), nullable=False, index=True) + prompt_index = db.Column(db.Integer, nullable=False, default=0) + optimized_prompt = db.Column(db.Text, nullable=False) + evaluation_json = db.Column(db.JSON, nullable=False) + batch_summary = db.Column(db.Text, nullable=True) + incremental_hints_json = db.Column(db.JSON, nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True) + + def evaluation_dict(self): + data = self.evaluation_json if isinstance(self.evaluation_json, dict) else {} + return data + + def to_api_item(self): + """单条符合对外 JSON 契约的条目。""" + ev = self.evaluation_dict() + return { + 'optimized_prompt': self.optimized_prompt, + 'evaluation': ev, + 'history_id': str(self.id), + 'timestamp': (self.created_at.isoformat() + 'Z') if self.created_at else None, + } + + def to_history_row(self): + ev = self.evaluation_dict() + score = ev.get('score') + return { + 'id': self.id, + 'batch_id': self.batch_id, + 'prompt_index': self.prompt_index, + 'preview': (self.optimized_prompt or '')[:120], + 'score': score, + 'created_at': self.created_at.isoformat() if self.created_at else None, + } diff --git a/src/flask_prompt_master/routes/comparison_routes.py b/src/flask_prompt_master/routes/comparison_routes.py new file mode 100644 index 0000000..354fc74 --- /dev/null +++ b/src/flask_prompt_master/routes/comparison_routes.py @@ -0,0 +1,502 @@ +# -*- coding: utf-8 -*- +""" +对比组管理路由 +提供对比组创建、管理功能API +""" + +from flask import Blueprint, request, jsonify, current_app +from src.flask_prompt_master import db +from src.flask_prompt_master.models import ComparisonGroup, PromptHistory, PromptEvaluation +from src.flask_prompt_master.user_context import get_current_user_id +from datetime import datetime +import uuid + +comparison_bp = Blueprint('comparison', __name__, url_prefix='/api/comparison') + + +def get_user_id_from_request(): + """从请求中获取用户ID(显式传入时优先)""" + user_id = request.headers.get('X-User-Id') or request.args.get('user_id') + + if not user_id and request.is_json: + data = request.get_json(silent=True) or {} + user_id = data.get('user_id') + + try: + return int(user_id) if user_id is not None and str(user_id).strip() != '' else None + except (TypeError, ValueError): + return None + + +def resolve_comparison_user_id(): + return get_user_id_from_request() or get_current_user_id() + + +@comparison_bp.route('/create', methods=['POST']) +def create_comparison_group(): + """创建对比组""" + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': '请求数据为空' + }), 400 + + user_id = resolve_comparison_user_id() or data.get('user_id') + if not user_id: + return jsonify({ + 'success': False, + 'message': '缺少必要参数:user_id' + }), 400 + + # 生成组ID + group_id = str(uuid.uuid4()) + + # 创建对比组(与 Vue 侧 group_name / group_description 字段对齐) + group = ComparisonGroup.create_group( + user_id=user_id, + group_id=group_id, + name=data.get('name') or data.get('group_name'), + description=data.get('description') or data.get('group_description'), + is_public=data.get('is_public', False) + ) + + # 如果有历史记录ID,添加到对比组 + history_ids = data.get('history_ids', []) + added_histories = [] + + for history_id in history_ids: + history = PromptHistory.query.get(history_id) + if history: + history.comparison_group_id = group_id + history.is_comparison_enabled = True + added_histories.append(history.to_dict()) + + if added_histories: + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '对比组创建成功', + 'data': { + 'group_id': group_id, + 'group': group.to_dict(), + 'added_histories': added_histories, + 'total_histories': len(added_histories) + } + }) + + except Exception as e: + current_app.logger.error(f'创建对比组失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'创建对比组失败: {str(e)}' + }), 500 + + +@comparison_bp.route('/groups', methods=['GET']) +def get_user_groups(): + """获取用户的对比组列表""" + try: + user_id = resolve_comparison_user_id() + if not user_id: + return jsonify({ + 'success': False, + 'message': '缺少必要参数:user_id' + }), 400 + + # 获取用户的对比组 + groups = ComparisonGroup.get_user_groups(user_id) + + # 为每个组添加统计信息 + for group in groups: + group_id = group['group_id'] + + # 获取组中的历史记录数量 + history_count = PromptHistory.query.filter_by(comparison_group_id=group_id).count() + group['history_count'] = history_count + + # 获取评价数量 + evaluation_count = PromptEvaluation.query.filter_by(comparison_group_id=group_id).count() + group['evaluation_count'] = evaluation_count + + # 获取最佳版本 + best_version = PromptEvaluation.query.filter_by( + comparison_group_id=group_id, + is_best_version=True + ).first() + group['has_best_version'] = best_version is not None + + # 获取最后活动时间 + last_history = PromptHistory.query.filter_by( + comparison_group_id=group_id + ).order_by(PromptHistory.created_at.desc()).first() + + group['last_activity'] = last_history.created_at.isoformat() if last_history else None + + return jsonify({ + 'success': True, + 'message': '获取对比组列表成功', + 'data': { + 'groups': groups, + 'total': len(groups) + } + }) + + except Exception as e: + current_app.logger.error(f'获取对比组列表失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'获取对比组列表失败: {str(e)}' + }), 500 + + +@comparison_bp.route('/', methods=['GET']) +def get_group_detail(group_id): + """获取对比组详情""" + try: + # 获取对比组 + group = ComparisonGroup.query.filter_by(group_id=group_id).first() + if not group: + return jsonify({ + 'success': False, + 'message': '对比组不存在' + }), 404 + + # 检查权限(如果是私有组) + user_id = resolve_comparison_user_id() + if not group.is_public and group.user_id != user_id: + return jsonify({ + 'success': False, + 'message': '无权访问此对比组' + }), 403 + + # 获取组中的历史记录 + histories = PromptHistory.query.filter_by(comparison_group_id=group_id).all() + history_list = [h.to_dict() for h in histories] + + # 为每个历史记录添加评价信息 + for history in history_list: + history_id = history['id'] + evaluations = PromptEvaluation.query.filter_by(history_id=history_id).all() + history['evaluations'] = [e.to_dict() for e in evaluations] + + # 计算平均评分 + if evaluations: + overall_ratings = [e.overall_rating for e in evaluations if e.overall_rating] + history['average_rating'] = sum(overall_ratings) / len(overall_ratings) if overall_ratings else 0 + else: + history['average_rating'] = 0 + + # 获取所有评价 + evaluations = PromptEvaluation.query.filter_by(comparison_group_id=group_id).all() + evaluation_list = [e.to_dict() for e in evaluations] + + # 获取最佳版本 + best_version = PromptEvaluation.query.filter_by( + comparison_group_id=group_id, + is_best_version=True + ).first() + best_version_data = best_version.to_dict() if best_version else None + + # 获取维度分析 + dimension_stats = { + 'clarity': {'ratings': [], 'average': 0, 'count': 0}, + 'specificity': {'ratings': [], 'average': 0, 'count': 0}, + 'effectiveness': {'ratings': [], 'average': 0, 'count': 0}, + 'professionalism': {'ratings': [], 'average': 0, 'count': 0}, + 'completeness': {'ratings': [], 'average': 0, 'count': 0} + } + + for eval in evaluations: + if eval.clarity_rating: + dimension_stats['clarity']['ratings'].append(eval.clarity_rating) + if eval.specificity_rating: + dimension_stats['specificity']['ratings'].append(eval.specificity_rating) + if eval.effectiveness_rating: + dimension_stats['effectiveness']['ratings'].append(eval.effectiveness_rating) + if eval.professionalism_rating: + dimension_stats['professionalism']['ratings'].append(eval.professionalism_rating) + if eval.completeness_rating: + dimension_stats['completeness']['ratings'].append(eval.completeness_rating) + + for dimension, data in dimension_stats.items(): + ratings = data['ratings'] + if ratings: + data['average'] = sum(ratings) / len(ratings) + data['count'] = len(ratings) + + return jsonify({ + 'success': True, + 'message': '获取对比组详情成功', + 'data': { + 'group': group.to_dict(), + 'histories': history_list, + 'evaluations': evaluation_list, + 'best_version': best_version_data, + 'statistics': { + 'total_histories': len(history_list), + 'total_evaluations': len(evaluation_list), + 'dimension_stats': dimension_stats + } + } + }) + + except Exception as e: + current_app.logger.error(f'获取对比组详情失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'获取对比组详情失败: {str(e)}' + }), 500 + + +@comparison_bp.route('//add', methods=['POST']) +def add_to_comparison_group(group_id): + """向对比组添加提示词""" + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': '请求数据为空' + }), 400 + + # 验证对比组是否存在 + group = ComparisonGroup.query.filter_by(group_id=group_id).first() + if not group: + return jsonify({ + 'success': False, + 'message': '对比组不存在' + }), 404 + + # 检查权限 + user_id = resolve_comparison_user_id() + if not group.is_public and group.user_id != user_id: + return jsonify({ + 'success': False, + 'message': '无权修改此对比组' + }), 403 + + history_ids = data.get('history_ids', []) + added_histories = [] + failed_histories = [] + + for history_id in history_ids: + history = PromptHistory.query.get(history_id) + if not history: + failed_histories.append({ + 'history_id': history_id, + 'reason': '历史记录不存在' + }) + continue + + # 检查是否已在其他对比组中 + if history.comparison_group_id and history.comparison_group_id != group_id: + failed_histories.append({ + 'history_id': history_id, + 'reason': '历史记录已属于其他对比组' + }) + continue + + # 添加到对比组 + history.comparison_group_id = group_id + history.is_comparison_enabled = True + added_histories.append(history.to_dict()) + + if added_histories: + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '添加提示词到对比组成功', + 'data': { + 'group_id': group_id, + 'added_histories': added_histories, + 'failed_histories': failed_histories, + 'total_added': len(added_histories), + 'total_failed': len(failed_histories) + } + }) + + except Exception as e: + current_app.logger.error(f'添加提示词到对比组失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'添加提示词到对比组失败: {str(e)}' + }), 500 + + +@comparison_bp.route('//remove', methods=['POST']) +def remove_from_comparison_group(group_id): + """从对比组移除提示词""" + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': '请求数据为空' + }), 400 + + # 验证对比组是否存在 + group = ComparisonGroup.query.filter_by(group_id=group_id).first() + if not group: + return jsonify({ + 'success': False, + 'message': '对比组不存在' + }), 404 + + # 检查权限 + user_id = resolve_comparison_user_id() + if not group.is_public and group.user_id != user_id: + return jsonify({ + 'success': False, + 'message': '无权修改此对比组' + }), 403 + + history_ids = data.get('history_ids', []) + removed_histories = [] + failed_histories = [] + + for history_id in history_ids: + history = PromptHistory.query.get(history_id) + if not history: + failed_histories.append({ + 'history_id': history_id, + 'reason': '历史记录不存在' + }) + continue + + # 检查是否属于此对比组 + if history.comparison_group_id != group_id: + failed_histories.append({ + 'history_id': history_id, + 'reason': '历史记录不属于此对比组' + }) + continue + + # 从对比组移除 + history.comparison_group_id = None + history.is_comparison_enabled = False + removed_histories.append(history.to_dict()) + + if removed_histories: + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '从对比组移除提示词成功', + 'data': { + 'group_id': group_id, + 'removed_histories': removed_histories, + 'failed_histories': failed_histories, + 'total_removed': len(removed_histories), + 'total_failed': len(failed_histories) + } + }) + + except Exception as e: + current_app.logger.error(f'从对比组移除提示词失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'从对比组移除提示词失败: {str(e)}' + }), 500 + + +@comparison_bp.route('/', methods=['DELETE']) +def delete_comparison_group(group_id): + """删除对比组""" + try: + # 验证对比组是否存在 + group = ComparisonGroup.query.filter_by(group_id=group_id).first() + if not group: + return jsonify({ + 'success': False, + 'message': '对比组不存在' + }), 404 + + # 检查权限 + user_id = resolve_comparison_user_id() + if group.user_id != user_id: + return jsonify({ + 'success': False, + 'message': '无权删除此对比组' + }), 403 + + # 移除对比组关联的历史记录 + histories = PromptHistory.query.filter_by(comparison_group_id=group_id).all() + for history in histories: + history.comparison_group_id = None + history.is_comparison_enabled = False + + # 删除对比组 + db.session.delete(group) + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '删除对比组成功', + 'data': { + 'group_id': group_id, + 'removed_histories_count': len(histories) + } + }) + + except Exception as e: + current_app.logger.error(f'删除对比组失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'删除对比组失败: {str(e)}' + }), 500 + + +@comparison_bp.route('//update', methods=['PUT']) +def update_comparison_group(group_id): + """更新对比组信息""" + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': '请求数据为空' + }), 400 + + # 验证对比组是否存在 + group = ComparisonGroup.query.filter_by(group_id=group_id).first() + if not group: + return jsonify({ + 'success': False, + 'message': '对比组不存在' + }), 404 + + # 检查权限 + user_id = resolve_comparison_user_id() + if group.user_id != user_id: + return jsonify({ + 'success': False, + 'message': '无权更新此对比组' + }), 403 + + # 更新字段 + if 'name' in data: + group.name = data['name'] + if 'description' in data: + group.description = data['description'] + if 'is_public' in data: + group.is_public = data['is_public'] + + group.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '更新对比组成功', + 'data': group.to_dict() + }) + + except Exception as e: + current_app.logger.error(f'更新对比组失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'更新对比组失败: {str(e)}' + }), 500 \ No newline at end of file diff --git a/src/flask_prompt_master/routes/evaluation_routes.py b/src/flask_prompt_master/routes/evaluation_routes.py new file mode 100644 index 0000000..285b3d8 --- /dev/null +++ b/src/flask_prompt_master/routes/evaluation_routes.py @@ -0,0 +1,519 @@ +# -*- coding: utf-8 -*- +""" +提示词评价路由 +提供多维度评价和对比功能API +""" + +from flask import Blueprint, request, jsonify, current_app +from src.flask_prompt_master import db +from src.flask_prompt_master.models import PromptEvaluation, ComparisonGroup, PromptHistory +from src.flask_prompt_master.user_context import get_current_user_id +from datetime import datetime, timedelta +import uuid + +evaluation_bp = Blueprint('evaluation', __name__, url_prefix='/api/evaluation') + + +def get_user_id_from_request(): + """从请求中获取用户ID(显式传入时优先)""" + user_id = request.headers.get('X-User-Id') or request.args.get('user_id') + + if not user_id and request.is_json: + data = request.get_json(silent=True) or {} + user_id = data.get('user_id') + + try: + return int(user_id) if user_id is not None and str(user_id).strip() != '' else None + except (TypeError, ValueError): + return None + + +def resolve_evaluation_user_id(): + """与历史/生成 API 一致:显式 user_id 优先,否则用 Session 当前用户。""" + return get_user_id_from_request() or get_current_user_id() + + +@evaluation_bp.route('/submit', methods=['POST']) +def submit_evaluation(): + """提交评价""" + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': '请求数据为空' + }), 400 + + # 验证必要字段 + history_id = data.get('history_id') + user_id = resolve_evaluation_user_id() or data.get('user_id') + + if not history_id or not user_id: + return jsonify({ + 'success': False, + 'message': '缺少必要参数:history_id 或 user_id' + }), 400 + + # 验证历史记录是否存在 + history = PromptHistory.query.get(history_id) + if not history: + return jsonify({ + 'success': False, + 'message': '历史记录不存在' + }), 404 + + # 准备评价数据 + evaluation_data = { + 'clarity_rating': data.get('clarity_rating'), + 'specificity_rating': data.get('specificity_rating'), + 'effectiveness_rating': data.get('effectiveness_rating'), + 'professionalism_rating': data.get('professionalism_rating'), + 'completeness_rating': data.get('completeness_rating'), + 'is_best_version': data.get('is_best_version', False), + 'comparison_group_id': data.get('comparison_group_id'), + 'overall_rating': data.get('overall_rating'), + 'comments': data.get('comments') + } + + # 如果标记为最佳版本,需要确保同组中只有一个最佳版本 + if evaluation_data.get('is_best_version') and evaluation_data.get('comparison_group_id'): + # 清除同组中其他记录的最佳版本标记 + evaluations = PromptEvaluation.query.filter_by( + comparison_group_id=evaluation_data['comparison_group_id'] + ).all() + + for row in evaluations: + if row.is_best_version: + row.is_best_version = False + + # 提交更改 + db.session.commit() + + # 提交评价 + evaluation = PromptEvaluation.submit_evaluation(history_id, user_id, evaluation_data) + + # 更新历史记录的平均评分(如果需要) + if evaluation_data.get('overall_rating'): + history.satisfaction_rating = evaluation_data['overall_rating'] + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '评价提交成功', + 'data': evaluation.to_dict() + }) + + except Exception as e: + current_app.logger.error(f'提交评价失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'提交评价失败: {str(e)}' + }), 500 + + +@evaluation_bp.route('/history/', methods=['GET']) +def get_history_evaluations(history_id): + """获取指定历史记录的评价""" + try: + evaluations = PromptEvaluation.get_history_evaluations(history_id) + + return jsonify({ + 'success': True, + 'message': '获取评价成功', + 'data': { + 'history_id': history_id, + 'evaluations': evaluations, + 'total': len(evaluations) + } + }) + + except Exception as e: + current_app.logger.error(f'获取历史记录评价失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'获取历史记录评价失败: {str(e)}' + }), 500 + + +@evaluation_bp.route('/list', methods=['GET']) +def list_user_evaluations(): + """当前用户的评价列表(含关联历史摘要,供评价历史页)""" + try: + user_id = resolve_evaluation_user_id() + page = request.args.get('page', 1, type=int) or 1 + per_page = min(request.args.get('per_page', 20, type=int) or 20, 100) + history_id = request.args.get('history_id', type=int) + days = request.args.get('days', type=int) + + query = PromptEvaluation.query.filter_by(user_id=user_id) + if history_id: + query = query.filter_by(history_id=history_id) + if days is not None and days > 0: + date_from = datetime.utcnow() - timedelta(days=days) + query = query.filter(PromptEvaluation.created_at >= date_from) + + query = query.order_by(PromptEvaluation.created_at.desc()) + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + + items = [] + for ev in pagination.items: + item = ev.to_dict() + hist = PromptHistory.query.get(ev.history_id) + if hist: + item['history'] = { + 'id': hist.id, + 'generated_prompt': hist.generated_prompt, + 'original_input': hist.original_input, + } + else: + item['history'] = None + items.append(item) + + return jsonify({ + 'success': True, + 'message': '获取评价列表成功', + 'data': { + 'evaluations': items, + 'total': pagination.total, + 'page': pagination.page, + 'per_page': pagination.per_page, + 'pages': pagination.pages, + } + }) + + except Exception as e: + current_app.logger.error(f'获取评价列表失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'获取评价列表失败: {str(e)}' + }), 500 + + +@evaluation_bp.route('/comparison/', methods=['GET']) +def get_comparison_evaluations(group_id): + """获取对比组评价""" + try: + evaluations = PromptEvaluation.get_comparison_group_evaluations(group_id) + best_version = PromptEvaluation.get_best_version_in_group(group_id) + + # 获取对比组信息 + group = ComparisonGroup.query.filter_by(group_id=group_id).first() + group_info = group.to_dict() if group else None + + # 获取对比组中的历史记录 + histories = PromptHistory.query.filter_by(comparison_group_id=group_id).all() + history_list = [h.to_dict() for h in histories] + + # 为每个历史记录添加评价信息 + for history in history_list: + history_id = history['id'] + history_evaluations = PromptEvaluation.query.filter_by(history_id=history_id).all() + history['evaluations'] = [e.to_dict() for e in history_evaluations] + history['average_rating'] = sum([e.overall_rating for e in history_evaluations if e.overall_rating]) / len([e for e in history_evaluations if e.overall_rating]) if history_evaluations else 0 + + return jsonify({ + 'success': True, + 'message': '获取对比组评价成功', + 'data': { + 'group_id': group_id, + 'group_info': group_info, + 'evaluations': evaluations, + 'best_version': best_version, + 'histories': history_list, + 'total_histories': len(history_list), + 'total_evaluations': len(evaluations) + } + }) + + except Exception as e: + current_app.logger.error(f'获取对比组评价失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'获取对比组评价失败: {str(e)}' + }), 500 + + +@evaluation_bp.route('/compare', methods=['POST']) +def batch_compare_evaluations(): + """批量对比评价""" + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': '请求数据为空' + }), 400 + + history_ids = data.get('history_ids', []) + user_id = resolve_evaluation_user_id() or data.get('user_id') + + if not history_ids or not user_id: + return jsonify({ + 'success': False, + 'message': '缺少必要参数:history_ids 或 user_id' + }), 400 + + # 验证所有历史记录是否存在 + histories = [] + for history_id in history_ids: + history = PromptHistory.query.get(history_id) + if not history: + return jsonify({ + 'success': False, + 'message': f'历史记录 {history_id} 不存在' + }), 404 + histories.append(history) + + # 创建对比组 + group_id = str(uuid.uuid4()) + group_name = data.get('group_name', f'对比组 {datetime.now().strftime("%Y-%m-%d %H:%M")}') + group_description = data.get('group_description', '') + + group = ComparisonGroup.create_group( + user_id=user_id, + group_id=group_id, + name=group_name, + description=group_description, + is_public=data.get('is_public', False) + ) + + # 将历史记录添加到对比组 + for history in histories: + history.comparison_group_id = group_id + history.is_comparison_enabled = True + + db.session.commit() + + # 返回对比组信息 + return jsonify({ + 'success': True, + 'message': '批量对比评价创建成功', + 'data': { + 'group_id': group_id, + 'group_info': group.to_dict(), + 'histories': [h.to_dict() for h in histories], + 'total_histories': len(histories) + } + }) + + except Exception as e: + current_app.logger.error(f'批量对比评价失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'批量对比评价失败: {str(e)}' + }), 500 + + +@evaluation_bp.route('/stats', methods=['GET']) +def get_evaluation_statistics(): + """获取评价统计信息""" + try: + user_id = get_user_id_from_request() or request.args.get('user_id') or get_current_user_id() + history_id = request.args.get('history_id') + days = request.args.get('days', 30, type=int) + + # 计算日期范围 + date_to = datetime.utcnow() + date_from = date_to - timedelta(days=days) + + # 获取统计信息 + stats = PromptEvaluation.get_evaluation_statistics( + history_id=history_id, + user_id=user_id, + date_from=date_from, + date_to=date_to + ) + + # 获取趋势数据(最近7天) + trend_data = [] + for i in range(7): + day_start = date_to - timedelta(days=i+1) + day_end = date_to - timedelta(days=i) + + day_stats = PromptEvaluation.get_evaluation_statistics( + history_id=history_id, + user_id=user_id, + date_from=day_start, + date_to=day_end + ) + + trend_data.append({ + 'date': day_start.strftime('%Y-%m-%d'), + 'total_evaluations': day_stats['total_evaluations'], + 'average_overall_rating': day_stats['average_overall_rating'] + }) + + trend_data.reverse() # 按时间顺序排列 + + return jsonify({ + 'success': True, + 'message': '获取评价统计成功', + 'data': { + 'statistics': stats, + 'trend_data': trend_data, + 'time_range': { + 'date_from': date_from.strftime('%Y-%m-%d'), + 'date_to': date_to.strftime('%Y-%m-%d'), + 'days': days + } + } + }) + + except Exception as e: + current_app.logger.error(f'获取评价统计失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'获取评价统计失败: {str(e)}' + }), 500 + + +@evaluation_bp.route('/dimension-analysis', methods=['GET']) +def get_dimension_analysis(): + """获取维度分析数据""" + try: + user_id = get_user_id_from_request() or request.args.get('user_id') or get_current_user_id() + days = request.args.get('days', 30, type=int) + + # 计算日期范围 + date_to = datetime.utcnow() + date_from = date_to - timedelta(days=days) + + # 获取评价数据 + query = PromptEvaluation.query + + if user_id: + query = query.filter_by(user_id=user_id) + + query = query.filter(PromptEvaluation.created_at >= date_from) + query = query.filter(PromptEvaluation.created_at <= date_to) + + evaluations = query.all() + + # 初始化维度数据 + dimension_data = { + 'clarity': {'ratings': [], 'average': 0, 'count': 0}, + 'specificity': {'ratings': [], 'average': 0, 'count': 0}, + 'effectiveness': {'ratings': [], 'average': 0, 'count': 0}, + 'professionalism': {'ratings': [], 'average': 0, 'count': 0}, + 'completeness': {'ratings': [], 'average': 0, 'count': 0} + } + + # 收集维度评分 + for row in evaluations: + if row.clarity_rating: + dimension_data['clarity']['ratings'].append(row.clarity_rating) + if row.specificity_rating: + dimension_data['specificity']['ratings'].append(row.specificity_rating) + if row.effectiveness_rating: + dimension_data['effectiveness']['ratings'].append(row.effectiveness_rating) + if row.professionalism_rating: + dimension_data['professionalism']['ratings'].append(row.professionalism_rating) + if row.completeness_rating: + dimension_data['completeness']['ratings'].append(row.completeness_rating) + + # 计算平均值 + for dimension, data in dimension_data.items(): + ratings = data['ratings'] + if ratings: + data['average'] = sum(ratings) / len(ratings) + data['count'] = len(ratings) + + # 准备图表数据 + chart_labels = list(dimension_data.keys()) + chart_averages = [dimension_data[d]['average'] for d in chart_labels] + chart_counts = [dimension_data[d]['count'] for d in chart_labels] + + return jsonify({ + 'success': True, + 'message': '获取维度分析成功', + 'data': { + 'dimension_data': dimension_data, + 'chart_data': { + 'labels': chart_labels, + 'averages': chart_averages, + 'counts': chart_counts + }, + 'time_range': { + 'date_from': date_from.strftime('%Y-%m-%d'), + 'date_to': date_to.strftime('%Y-%m-%d'), + 'days': days + } + } + }) + + except Exception as e: + current_app.logger.error(f'获取维度分析失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'获取维度分析失败: {str(e)}' + }), 500 + + +@evaluation_bp.route('/best-version', methods=['POST']) +def mark_best_version(): + """标记最佳版本""" + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': '请求数据为空' + }), 400 + + evaluation_id = data.get('evaluation_id') + comparison_group_id = data.get('comparison_group_id') + user_id = resolve_evaluation_user_id() or data.get('user_id') + + if not evaluation_id or not comparison_group_id or not user_id: + return jsonify({ + 'success': False, + 'message': '缺少必要参数:evaluation_id, comparison_group_id 或 user_id' + }), 400 + + # 获取评价 + evaluation = PromptEvaluation.query.get(evaluation_id) + if not evaluation: + return jsonify({ + 'success': False, + 'message': '评价不存在' + }), 404 + + # 验证权限 + if evaluation.user_id != user_id: + return jsonify({ + 'success': False, + 'message': '无权修改此评价' + }), 403 + + # 验证对比组 + if evaluation.comparison_group_id != comparison_group_id: + return jsonify({ + 'success': False, + 'message': '评价不属于此对比组' + }), 400 + + # 清除同组中其他记录的最佳版本标记 + evaluations = PromptEvaluation.query.filter_by( + comparison_group_id=comparison_group_id + ).all() + + for eval_item in evaluations: + if eval_item.is_best_version: + eval_item.is_best_version = False + + # 设置当前评价为最佳版本 + evaluation.is_best_version = True + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '标记最佳版本成功', + 'data': evaluation.to_dict() + }) + + except Exception as e: + current_app.logger.error(f'标记最佳版本失败: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'标记最佳版本失败: {str(e)}' + }), 500 \ No newline at end of file diff --git a/src/flask_prompt_master/routes/prompt_quality_routes.py b/src/flask_prompt_master/routes/prompt_quality_routes.py new file mode 100644 index 0000000..1074fee --- /dev/null +++ b/src/flask_prompt_master/routes/prompt_quality_routes.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +"""提示词结构化质量评价 API""" + +import uuid +from datetime import datetime, timedelta + +from flask import Blueprint, jsonify, request, current_app +from sqlalchemy import desc + +from src.flask_prompt_master import db +from src.flask_prompt_master.models.prompt_quality_models import PromptQualityRecord +from src.flask_prompt_master.services.prompt_quality_service import analyze_prompts +from src.flask_prompt_master.user_context import get_current_user_id + +prompt_quality_bp = Blueprint('prompt_quality', __name__, url_prefix='/api/prompt-quality') + + +@prompt_quality_bp.route('/analyze', methods=['POST']) +def analyze(): + """多提示词 → 模型评价 → 落库 → 返回契约 JSON。""" + data = request.get_json(silent=True) or {} + prompts = data.get('prompts') + task_context = (data.get('task_context') or '').strip() or None + + if not isinstance(prompts, list): + return jsonify({'success': False, 'message': 'prompts 须为非空数组'}), 400 + + try: + parsed = analyze_prompts(prompts, task_context) + except ValueError as e: + return jsonify({'success': False, 'message': str(e)}), 400 + except Exception as e: + current_app.logger.exception('prompt-quality analyze') + return jsonify({'success': False, 'message': f'分析失败: {e}'}), 500 + + user_id = get_current_user_id() + batch_id = str(uuid.uuid4()) + results_out = [] + summary = parsed.get('batch_comparison') or '' + hints = parsed.get('incremental_learning_hints') or [] + + try: + for idx, item in enumerate(parsed['results']): + rec = PromptQualityRecord( + user_id=user_id, + batch_id=batch_id, + prompt_index=idx, + optimized_prompt=item['optimized_prompt'], + evaluation_json=item['evaluation'], + batch_summary=summary if idx == 0 else None, + incremental_hints_json=hints if idx == 0 else None, + ) + db.session.add(rec) + db.session.commit() + except Exception as e: + db.session.rollback() + current_app.logger.exception('prompt-quality persist') + return jsonify({'success': False, 'message': f'保存评价失败: {e}'}), 500 + + rows = ( + PromptQualityRecord.query.filter_by(batch_id=batch_id) + .order_by(PromptQualityRecord.prompt_index.asc()) + .all() + ) + for rec in rows: + results_out.append(rec.to_api_item()) + + return jsonify( + { + 'success': True, + 'data': { + 'batch_id': batch_id, + 'results': results_out, + 'batch_comparison': summary, + 'incremental_learning_hints': hints, + }, + } + ) + + +@prompt_quality_bp.route('/history', methods=['GET']) +def list_history(): + """按用户、时间、关键词、批次筛选;按记录分页(含 batch_id 便于前端聚合)。""" + user_id = get_current_user_id() + page = request.args.get('page', 1, type=int) or 1 + per_page = min(request.args.get('per_page', 15, type=int) or 15, 50) + q = (request.args.get('q') or '').strip() + batch_id = (request.args.get('batch_id') or '').strip() or None + date_from = request.args.get('date_from') + date_to = request.args.get('date_to') + + base = PromptQualityRecord.query.filter(PromptQualityRecord.user_id == user_id) + if batch_id: + base = base.filter(PromptQualityRecord.batch_id == batch_id) + if q: + base = base.filter(PromptQualityRecord.optimized_prompt.contains(q)) + if date_from: + try: + d0 = datetime.fromisoformat(date_from.replace('Z', '+00:00')) + if d0.tzinfo: + d0 = d0.replace(tzinfo=None) + base = base.filter(PromptQualityRecord.created_at >= d0) + except ValueError: + return jsonify({'success': False, 'message': 'date_from 格式无效'}), 400 + if date_to: + try: + d1 = datetime.fromisoformat(date_to.replace('Z', '+00:00')) + if d1.tzinfo: + d1 = d1.replace(tzinfo=None) + base = base.filter(PromptQualityRecord.created_at < d1 + timedelta(days=1)) + except ValueError: + return jsonify({'success': False, 'message': 'date_to 格式无效'}), 400 + + base = base.order_by(desc(PromptQualityRecord.created_at)) + pagination = base.paginate(page=page, per_page=per_page, error_out=False) + items = [r.to_history_row() for r in pagination.items] + + return jsonify( + { + 'success': True, + 'data': { + 'items': items, + 'pagination': { + 'page': pagination.page, + 'per_page': pagination.per_page, + 'total': pagination.total, + 'pages': pagination.pages, + 'has_next': pagination.has_next, + 'has_prev': pagination.has_prev, + }, + }, + } + ) + + +@prompt_quality_bp.route('/history/', methods=['GET']) +def get_record(record_id: int): + user_id = get_current_user_id() + rec = PromptQualityRecord.query.filter_by(id=record_id, user_id=user_id).first() + if not rec: + return jsonify({'success': False, 'message': '记录不存在'}), 404 + + batch = ( + PromptQualityRecord.query.filter_by(batch_id=rec.batch_id, user_id=user_id) + .order_by(PromptQualityRecord.prompt_index.asc()) + .all() + ) + results = [r.to_api_item() for r in batch] + summary = next((r.batch_summary for r in batch if r.batch_summary), '') or '' + hints = next((r.incremental_hints_json for r in batch if r.incremental_hints_json), []) or [] + + return jsonify( + { + 'success': True, + 'data': { + 'batch_id': rec.batch_id, + 'results': results, + 'batch_comparison': summary, + 'incremental_learning_hints': hints if isinstance(hints, list) else [], + }, + } + ) + + +@prompt_quality_bp.route('/history/batch/', methods=['GET']) +def get_batch(batch_uuid: str): + """按 batch_id 取整批详情(可视化对比)。""" + user_id = get_current_user_id() + batch = ( + PromptQualityRecord.query.filter_by(batch_id=batch_uuid, user_id=user_id) + .order_by(PromptQualityRecord.prompt_index.asc()) + .all() + ) + if not batch: + return jsonify({'success': False, 'message': '批次不存在'}), 404 + results = [r.to_api_item() for r in batch] + summary = next((r.batch_summary for r in batch if r.batch_summary), '') or '' + hints = next((r.incremental_hints_json for r in batch if r.incremental_hints_json), []) or [] + return jsonify( + { + 'success': True, + 'data': { + 'batch_id': batch_uuid, + 'results': results, + 'batch_comparison': summary, + 'incremental_learning_hints': hints if isinstance(hints, list) else [], + }, + } + ) diff --git a/src/flask_prompt_master/services/prompt_quality_service.py b/src/flask_prompt_master/services/prompt_quality_service.py new file mode 100644 index 0000000..d729c56 --- /dev/null +++ b/src/flask_prompt_master/services/prompt_quality_service.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +"""调用 LLM 生成结构化提示词评价(JSON)。""" + +from __future__ import annotations + +import json +import logging +import re +from typing import Any + +from openai import OpenAI +import os + +logger = logging.getLogger(__name__) + +MAX_PROMPTS = 8 +MAX_CHARS_PER_PROMPT = 8000 + +SYSTEM_PROMPT = """你是提示词质量评估专家,熟悉 CRISP、结构化指令与可执行性等实践。 +用户会提供若干条「已优化后的提示词」(可能相互独立或用于同一任务对比)。 + +你必须只输出一个 JSON 对象(不要 Markdown 代码块、不要前后说明文字),结构为: +{ + "results": [ + { + "optimized_prompt": "与输入第 i 条完全一致的原文(逐字复制)", + "evaluation": { + "score": <0-100 的整数,百分制>, + "strengths": ["优势要点"], + "weaknesses": ["不足要点"], + "suggestions": ["综合改进建议"], + "applicability": ["适用场景/不适用场景简述"], + "mandatory_fixes": ["必须修正的问题:会导致指令歧义、无法执行或严重偏离预期输出"], + "optimization_suggestions": ["可选优化:语气、结构、示例、约束细化等"] + } + } + ], + "batch_comparison": "对这些提示词的横向对比结论(客观、简洁)", + "incremental_learning_hints": ["可迁移到其他提示词设计的一条条短经验"] +} + +评价维度必须体现:清晰度、指令完整性、可操作性、与预期输出的匹配度。 +mandatory_fixes 与 optimization_suggestions 不得混用:前者是「不修正则不合格」,后者是「改了更好」。 +若只有一条提示词,batch_comparison 可简要总结该条定位;incremental_learning_hints 至少 1 条。""" + + +def _client() -> OpenAI: + return OpenAI( + api_key=os.environ.get('LLM_API_KEY') or '', + base_url=os.environ.get('LLM_API_URL') or 'https://api.deepseek.com/v1', + ) + + +def _strip_json_fence(text: str) -> str: + text = text.strip() + m = re.match(r'^```(?:json)?\s*([\s\S]*?)\s*```$', text, re.IGNORECASE) + if m: + return m.group(1).strip() + return text + + +def _default_evaluation() -> dict[str, Any]: + return { + 'score': 0, + 'strengths': [], + 'weaknesses': [], + 'suggestions': [], + 'applicability': [], + 'mandatory_fixes': [], + 'optimization_suggestions': [], + } + + +def _normalize_result_item(raw: dict[str, Any], fallback_prompt: str) -> dict[str, Any]: + op = (raw.get('optimized_prompt') or fallback_prompt or '').strip() + ev = raw.get('evaluation') if isinstance(raw.get('evaluation'), dict) else {} + base = _default_evaluation() + base.update({k: ev.get(k, base[k]) for k in base}) + try: + sc = int(base['score']) + base['score'] = max(0, min(100, sc)) + except (TypeError, ValueError): + base['score'] = 0 + for key in ('strengths', 'weaknesses', 'suggestions', 'applicability', 'mandatory_fixes', 'optimization_suggestions'): + v = base.get(key) + if not isinstance(v, list): + base[key] = [] + else: + base[key] = [str(x).strip() for x in v if str(x).strip()] + return {'optimized_prompt': op, 'evaluation': base} + + +def analyze_prompts(prompts: list[str], task_context: str | None = None) -> dict[str, Any]: + """ + 调用模型分析多条提示词。 + 返回: { results: [{optimized_prompt, evaluation}], batch_comparison, incremental_learning_hints } + """ + cleaned: list[str] = [] + for p in prompts: + t = (p or '').strip() + if not t: + continue + cleaned.append(t[:MAX_CHARS_PER_PROMPT]) + if not cleaned: + raise ValueError('至少需要一条非空提示词') + if len(cleaned) > MAX_PROMPTS: + raise ValueError(f'单次最多 {MAX_PROMPTS} 条') + + numbered = '\n\n'.join(f'### 第 {i + 1} 条\n{c}' for i, c in enumerate(cleaned)) + user_parts = [numbered] + if task_context and task_context.strip(): + user_parts.insert(0, f'【任务/领域背景】\n{task_context.strip()}\n') + + user_content = '\n'.join(user_parts) + + client = _client() + kwargs: dict[str, Any] = { + 'model': 'deepseek-chat', + 'messages': [ + {'role': 'system', 'content': SYSTEM_PROMPT}, + {'role': 'user', 'content': user_content}, + ], + 'temperature': 0.35, + 'max_tokens': 4096, + 'timeout': 120, + } + try: + resp = client.chat.completions.create(**kwargs, response_format={'type': 'json_object'}) + except Exception as e: + logger.warning('json_object 模式不可用,回退普通输出: %s', e) + resp = client.chat.completions.create(**kwargs) + + text = (resp.choices[0].message.content or '').strip() + text = _strip_json_fence(text) + try: + data = json.loads(text) + except json.JSONDecodeError as e: + logger.error('评价 JSON 解析失败: %s', e) + raise ValueError('模型返回非合法 JSON,请重试') from e + + if not isinstance(data, dict): + raise ValueError('模型返回格式错误') + + raw_results = data.get('results') + if not isinstance(raw_results, list): + raw_results = [] + + results: list[dict[str, Any]] = [] + for i, fallback in enumerate(cleaned): + raw_item = raw_results[i] if i < len(raw_results) and isinstance(raw_results[i], dict) else {} + results.append(_normalize_result_item(raw_item, fallback)) + + hints = data.get('incremental_learning_hints') + if not isinstance(hints, list): + hints = [] + hints = [str(x).strip() for x in hints if str(x).strip()] + + comparison = data.get('batch_comparison') + if comparison is None or not isinstance(comparison, str): + comparison = '' + + return { + 'results': results, + 'batch_comparison': comparison.strip(), + 'incremental_learning_hints': hints, + } diff --git a/src/flask_prompt_master/static/css/evaluation.css b/src/flask_prompt_master/static/css/evaluation.css new file mode 100644 index 0000000..72c5962 --- /dev/null +++ b/src/flask_prompt_master/static/css/evaluation.css @@ -0,0 +1,394 @@ +/* 提示词评价功能专用样式 */ + +/* 版本卡片样式 */ +.version-card { + border: 1px solid #e2e8f0; + border-radius: 12px; + background: white; + transition: all 0.3s ease; + overflow: hidden; + position: relative; +} + +.version-card:hover { + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + border-color: #3B82F6; +} + +.version-card.best-version { + border: 2px solid #10B981; + background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(16, 185, 129, 0.02)); +} + +.version-card.best-version::before { + content: "最佳版本"; + position: absolute; + top: 10px; + right: -30px; + background: #10B981; + color: white; + padding: 4px 35px; + font-size: 0.75rem; + font-weight: 600; + transform: rotate(45deg); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* 评价维度样式 */ +.dimension-rating { + padding: 12px 0; + border-bottom: 1px solid #f1f5f9; +} + +.dimension-rating:last-child { + border-bottom: none; +} + +.dimension-label { + font-weight: 600; + color: #334155; + font-size: 0.95rem; +} + +.dimension-description { + font-size: 0.85rem; + color: #64748B; + margin-top: 2px; +} + +/* 星级评分样式 */ +.rating-stars { + color: #F59E0B; + font-size: 1.5rem; + letter-spacing: 2px; +} + +.rating-star { + cursor: pointer; + transition: all 0.2s ease; +} + +.rating-star:hover { + transform: scale(1.2); +} + +.rating-star.fas { + color: #F59E0B; +} + +.rating-star.far { + color: #CBD5E1; +} + +/* 步骤指示器样式 */ +.step-indicator { + display: flex; + justify-content: space-between; + position: relative; + margin: 40px 0; +} + +.step-indicator::before { + content: ''; + position: absolute; + top: 20px; + left: 0; + right: 0; + height: 2px; + background: #E2E8F0; + z-index: 1; +} + +.step { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 2; +} + +.step-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background: #E2E8F0; + color: #64748B; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; + transition: all 0.3s ease; +} + +.step.active .step-icon { + background: #3B82F6; + color: white; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); +} + +.step.completed .step-icon { + background: #10B981; + color: white; +} + +.step-label { + font-size: 0.9rem; + font-weight: 500; + color: #64748B; + text-align: center; +} + +.step.active .step-label { + color: #3B82F6; + font-weight: 600; +} + +.step.completed .step-label { + color: #10B981; +} + +/* 模板选择卡片 */ +.template-card { + border: 2px solid #E2E8F0; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + height: 100%; +} + +.template-card:hover { + border-color: #3B82F6; + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(59, 130, 246, 0.1); +} + +.template-card.selected { + border-color: #3B82F6; + background: rgba(59, 130, 246, 0.05); +} + +.template-checkbox:checked + label .template-card { + border-color: #3B82F6; + background: rgba(59, 130, 246, 0.05); +} + +/* 评价摘要卡片 */ +.evaluation-summary-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 15px; + overflow: hidden; +} + +.evaluation-summary-card .card-body { + padding: 30px; +} + +/* 维度分布图样式 */ +.dimension-pill { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border-radius: 20px; + padding: 6px 16px; + font-size: 0.85rem; + font-weight: 500; + display: inline-block; + margin: 0 8px 8px 0; + transition: all 0.3s ease; +} + +.dimension-pill:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +/* 对比组卡片 */ +.comparison-group-card { + border: 1px solid #E2E8F0; + border-radius: 12px; + transition: all 0.3s ease; + overflow: hidden; + height: 100%; +} + +.comparison-group-card:hover { + border-color: #8B5CF6; + box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.1); +} + +.comparison-group-card .card-header { + background: linear-gradient(135deg, #8B5CF6, #6366F1); + color: white; + border-bottom: none; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .step-indicator { + flex-wrap: wrap; + gap: 20px; + } + + .step-indicator::before { + display: none; + } + + .step { + flex: 0 0 calc(50% - 20px); + } + + .version-card { + margin-bottom: 20px; + } +} + +/* 评分徽章样式 */ +.rating-badge { + font-size: 0.85rem; + padding: 6px 12px; + border-radius: 20px; + font-weight: 600; +} + +.rating-badge.excellent { + background: linear-gradient(135deg, #10B981, #34D399); + color: white; +} + +.rating-badge.good { + background: linear-gradient(135deg, #3B82F6, #60A5FA); + color: white; +} + +.rating-badge.average { + background: linear-gradient(135deg, #F59E0B, #FBBF24); + color: white; +} + +.rating-badge.poor { + background: linear-gradient(135deg, #EF4444, #F87171); + color: white; +} + +/* 加载动画 */ +.evaluation-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; +} + +.evaluation-loading-spinner { + width: 50px; + height: 50px; + border: 3px solid #E2E8F0; + border-top-color: #3B82F6; + border-radius: 50%; + animation: evaluation-spin 1s linear infinite; + margin-bottom: 20px; +} + +@keyframes evaluation-spin { + to { transform: rotate(360deg); } +} + +/* 空状态样式 */ +.evaluation-empty-state { + text-align: center; + padding: 60px 20px; + color: #64748B; +} + +.evaluation-empty-state-icon { + font-size: 4rem; + color: #CBD5E1; + margin-bottom: 20px; +} + +/* 工具提示样式 */ +.evaluation-tooltip { + position: relative; + display: inline-block; + cursor: help; +} + +.evaluation-tooltip .tooltip-text { + visibility: hidden; + width: 200px; + background-color: #334155; + color: white; + text-align: center; + border-radius: 6px; + padding: 8px; + position: absolute; + z-index: 1000; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 0.3s; + font-size: 0.85rem; + line-height: 1.4; +} + +.evaluation-tooltip .tooltip-text::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #334155 transparent transparent transparent; +} + +.evaluation-tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + +/* 对比视图样式 */ +.comparison-view { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.comparison-column { + background: white; + border: 1px solid #E2E8F0; + border-radius: 10px; + padding: 20px; +} + +.comparison-column-header { + text-align: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 2px solid #F1F5F9; +} + +.comparison-column.best { + border-color: #10B981; + box-shadow: 0 8px 16px rgba(16, 185, 129, 0.1); +} + +/* 打印样式 */ +@media print { + .no-print { + display: none !important; + } + + .version-card { + break-inside: avoid; + border: 1px solid #ddd; + box-shadow: none; + } + + .rating-stars { + color: #666; + } +} \ No newline at end of file diff --git a/src/flask_prompt_master/static/js/evaluation.js b/src/flask_prompt_master/static/js/evaluation.js new file mode 100644 index 0000000..9b4b5e2 --- /dev/null +++ b/src/flask_prompt_master/static/js/evaluation.js @@ -0,0 +1,512 @@ +/** + * 提示词评价功能JavaScript模块 + * 提供评价表单提交、对比组管理、评价历史加载等功能 + */ + +const EvaluationManager = (function() { + 'use strict'; + + // 配置 + const config = { + apiBase: '/api/evaluation', + comparisonApiBase: '/api/comparison' + }; + + // 评价维度定义 + const dimensions = [ + { id: 'clarity_rating', label: '清晰度', description: '提示词表达是否清晰易懂' }, + { id: 'specificity_rating', label: '具体性', description: '提示词是否具体明确,无歧义' }, + { id: 'effectiveness_rating', label: '有效性', description: '提示词是否能获得良好响应' }, + { id: 'professionalism_rating', label: '专业性', description: '提示词是否符合专业规范' }, + { id: 'completeness_rating', label: '完整性', description: '提示词是否包含必要要素' } + ]; + + // 私有方法 + const _getCsrfToken = function() { + const meta = document.querySelector('meta[name="csrf-token"]'); + return meta ? meta.getAttribute('content') : ''; + }; + + const _showToast = function(message, type = 'success') { + // 简单的toast通知实现 + const toast = document.createElement('div'); + toast.className = `alert alert-${type} alert-dismissible fade show position-fixed`; + toast.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;'; + toast.innerHTML = ` + ${message} + + `; + document.body.appendChild(toast); + setTimeout(() => { + toast.remove(); + }, 5000); + }; + + const _handleApiError = function(error, defaultMessage = '操作失败') { + console.error('API Error:', error); + const message = error.response?.data?.message || error.message || defaultMessage; + _showToast(message, 'danger'); + return Promise.reject(error); + }; + + // 公共API + return { + /** + * 初始化评价表单 + * @param {string} formId - 表单元素ID + * @param {Object} options - 配置选项 + */ + initEvaluationForm(formId, options = {}) { + const form = document.getElementById(formId); + if (!form) { + console.warn(`表单 ${formId} 不存在`); + return; + } + + const defaults = { + historyId: null, + comparisonGroupId: null, + onSuccess: null, + onError: null + }; + const settings = { ...defaults, ...options }; + + // 初始化星级评分 + form.querySelectorAll('.star-rating').forEach(container => { + const input = container.querySelector('input[type="hidden"]'); + const stars = container.querySelectorAll('.star'); + + if (!input || !stars.length) return; + + // 设置初始值 + const initialValue = parseInt(input.value) || 0; + this._updateStars(stars, initialValue); + + // 添加点击事件 + stars.forEach((star, index) => { + star.addEventListener('click', () => { + const value = index + 1; + input.value = value; + this._updateStars(stars, value); + }); + }); + }); + + // 处理表单提交 + form.addEventListener('submit', async (e) => { + e.preventDefault(); + + const submitBtn = form.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + try { + // 禁用提交按钮 + submitBtn.disabled = true; + submitBtn.textContent = '提交中...'; + + // 收集表单数据 + const formData = new FormData(form); + const data = {}; + + // 转换表单数据 + for (const [key, value] of formData.entries()) { + if (key.endsWith('_rating')) { + data[key] = parseInt(value) || null; + } else if (key === 'is_best_version') { + data[key] = value === 'on' || value === 'true'; + } else { + data[key] = value; + } + } + + // 添加必要的参数 + if (settings.historyId) { + data.history_id = settings.historyId; + } + if (settings.comparisonGroupId) { + data.comparison_group_id = settings.comparisonGroupId; + } + + // 计算综合评分(各维度的平均值) + const ratingKeys = dimensions.map(d => d.id); + const ratings = ratingKeys.map(key => data[key]).filter(r => r !== null); + if (ratings.length > 0) { + data.overall_rating = Math.round(ratings.reduce((a, b) => a + b, 0) / ratings.length); + } + + // 发送API请求 + const response = await fetch(`${config.apiBase}/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': _getCsrfToken() + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + _showToast('评价提交成功', 'success'); + + // 调用成功回调 + if (typeof settings.onSuccess === 'function') { + settings.onSuccess(result.data); + } + + // 如果是模态框,关闭它 + const modal = form.closest('.modal'); + if (modal) { + const bootstrapModal = bootstrap.Modal.getInstance(modal); + if (bootstrapModal) { + bootstrapModal.hide(); + } + } + } else { + _showToast(result.message || '评价提交失败', 'danger'); + + // 调用错误回调 + if (typeof settings.onError === 'function') { + settings.onError(result); + } + } + } catch (error) { + _handleApiError(error, '提交评价时发生错误'); + + if (typeof settings.onError === 'function') { + settings.onError(error); + } + } finally { + // 恢复提交按钮 + submitBtn.disabled = false; + submitBtn.textContent = originalText; + } + }); + }, + + /** + * 创建对比组 + * @param {Array} historyIds - 历史记录ID数组 + * @param {Object} options - 配置选项 + */ + async createComparisonGroup(historyIds, options = {}) { + try { + const data = { + history_ids: historyIds, + group_name: options.groupName || `对比组 ${new Date().toLocaleString()}`, + group_description: options.description || '', + is_public: options.isPublic || false, + user_id: options.userId || null + }; + + const response = await fetch(`${config.apiBase}/compare`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': _getCsrfToken() + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + _showToast('对比组创建成功', 'success'); + return result.data; + } else { + throw new Error(result.message || '创建对比组失败'); + } + } catch (error) { + return _handleApiError(error, '创建对比组时发生错误'); + } + }, + + /** + * 获取对比组详情 + * @param {string} groupId - 对比组ID + */ + async getComparisonGroup(groupId) { + try { + const response = await fetch(`${config.comparisonApiBase}/${groupId}`); + const result = await response.json(); + + if (result.success) { + return result.data; + } else { + throw new Error(result.message || '获取对比组详情失败'); + } + } catch (error) { + return _handleApiError(error, '获取对比组详情时发生错误'); + } + }, + + /** + * 获取历史记录的评价 + * @param {number} historyId - 历史记录ID + */ + async getHistoryEvaluations(historyId) { + try { + const response = await fetch(`${config.apiBase}/history/${historyId}`); + const result = await response.json(); + + if (result.success) { + return result.data; + } else { + throw new Error(result.message || '获取评价失败'); + } + } catch (error) { + return _handleApiError(error, '获取评价时发生错误'); + } + }, + + /** + * 获取评价统计信息 + * @param {Object} filters - 过滤条件 + */ + async getEvaluationStatistics(filters = {}) { + try { + const params = new URLSearchParams(); + Object.entries(filters).forEach(([key, value]) => { + if (value !== null && value !== undefined) { + params.append(key, value); + } + }); + + const response = await fetch(`${config.apiBase}/stats?${params}`); + const result = await response.json(); + + if (result.success) { + return result.data; + } else { + throw new Error(result.message || '获取统计信息失败'); + } + } catch (error) { + return _handleApiError(error, '获取统计信息时发生错误'); + } + }, + + /** + * 标记最佳版本 + * @param {number} evaluationId - 评价ID + * @param {string} groupId - 对比组ID + */ + async markAsBestVersion(evaluationId, groupId) { + try { + const response = await fetch(`${config.apiBase}/best-version`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': _getCsrfToken() + }, + body: JSON.stringify({ + evaluation_id: evaluationId, + comparison_group_id: groupId + }) + }); + + const result = await response.json(); + + if (result.success) { + _showToast('已标记为最佳版本', 'success'); + return result.data; + } else { + throw new Error(result.message || '标记最佳版本失败'); + } + } catch (error) { + return _handleApiError(error, '标记最佳版本时发生错误'); + } + }, + + /** + * 渲染对比组视图 + * @param {string} containerId - 容器元素ID + * @param {Object} groupData - 对比组数据 + */ + renderComparisonView(containerId, groupData) { + const container = document.getElementById(containerId); + if (!container) { + console.warn(`容器 ${containerId} 不存在`); + return; + } + + const { group_info, histories, evaluations, best_version } = groupData; + + // 清空容器 + container.innerHTML = ''; + + // 渲染对比组标题 + const header = document.createElement('div'); + header.className = 'mb-4'; + header.innerHTML = ` +

${group_info?.name || '对比组'}

+ ${group_info?.description ? `

${group_info.description}

` : ''} + `; + container.appendChild(header); + + // 渲染版本卡片 + histories.forEach(history => { + const historyEvals = evaluations.filter(e => e.history_id === history.id); + const avgRating = historyEvals.length > 0 + ? historyEvals.reduce((sum, e) => sum + (e.overall_rating || 0), 0) / historyEvals.length + : 0; + + const isBestVersion = best_version && best_version.history_id === history.id; + + const card = document.createElement('div'); + card.className = `version-card mb-3 ${isBestVersion ? 'best-version' : ''}`; + card.innerHTML = ` +
+
+
${history.optimized_prompt ? history.optimized_prompt.substring(0, 100) + '...' : '无提示词'}
+
+ 创建时间: ${new Date(history.created_at).toLocaleString()} +
+ ${historyEvals.length > 0 ? ` +
+ 综合评分: ${avgRating.toFixed(1)}/5 +
+ ${this._renderStars(avgRating)} +
+
+ ` : '
尚未评价
'} +
+ ${isBestVersion ? ` + 最佳版本 + ` : ''} +
+ ${historyEvals.length > 0 ? this._renderEvaluationDetails(historyEvals) : ''} + `; + container.appendChild(card); + }); + }, + + /** + * 渲染评价详情 + * @param {Array} evaluations - 评价数组 + */ + _renderEvaluationDetails(evaluations) { + const latestEval = evaluations[evaluations.length - 1]; // 取最新评价 + + return ` +
+
最新评价
+
+ ${dimensions.map(dim => ` +
+
+ ${dim.label}: +
+ ${this._renderStars(latestEval[dim.id] || 0)} +
+ ${latestEval[dim.id] || 0}/5 +
+
+ `).join('')} +
+ ${latestEval.comments ? ` +
+ 评价意见: +

${latestEval.comments}

+
+ ` : ''} +
+ `; + }, + + /** + * 渲染星级 + * @param {number} rating - 评分 (0-5) + * @returns {string} HTML字符串 + */ + _renderStars(rating) { + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 >= 0.5; + const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); + + let stars = ''; + for (let i = 0; i < fullStars; i++) { + stars += ''; + } + if (hasHalfStar) { + stars += ''; + } + for (let i = 0; i < emptyStars; i++) { + stars += ''; + } + return stars; + }, + + /** + * 更新星级显示 + * @param {NodeList} stars - 星星元素列表 + * @param {number} value - 当前值 + */ + _updateStars(stars, value) { + stars.forEach((star, index) => { + if (index < value) { + star.classList.remove('far', 'fa-star-half-alt'); + star.classList.add('fas', 'fa-star'); + } else { + star.classList.remove('fas', 'fa-star-half-alt'); + star.classList.add('far', 'fa-star'); + } + }); + }, + + /** + * 获取所有评价维度 + * @returns {Array} 维度数组 + */ + getDimensions() { + return [...dimensions]; + } + }; +})(); + +// 全局初始化 +document.addEventListener('DOMContentLoaded', function() { + // 自动初始化所有评价表单 + document.querySelectorAll('[data-evaluation-form]').forEach(form => { + const formId = form.id || `evaluation-form-${Math.random().toString(36).substr(2, 9)}`; + if (!form.id) form.id = formId; + + const historyId = form.dataset.historyId; + const groupId = form.dataset.comparisonGroupId; + + EvaluationManager.initEvaluationForm(formId, { + historyId: historyId ? parseInt(historyId) : null, + comparisonGroupId: groupId || null + }); + }); + + // 初始化对比组查看器 + document.querySelectorAll('[data-comparison-group]').forEach(container => { + const groupId = container.dataset.comparisonGroupId; + if (groupId) { + EvaluationManager.getComparisonGroup(groupId) + .then(groupData => { + EvaluationManager.renderComparisonView(container.id, groupData); + }) + .catch(error => { + console.error('加载对比组失败:', error); + }); + } + }); + + // 初始化评价历史查看器 + document.querySelectorAll('[data-evaluation-history]').forEach(container => { + const historyId = container.dataset.historyId; + if (historyId) { + EvaluationManager.getHistoryEvaluations(parseInt(historyId)) + .then(data => { + container.innerHTML = EvaluationManager._renderEvaluationDetails(data.evaluations || []); + }) + .catch(error => { + console.error('加载评价历史失败:', error); + }); + } + }); +}); + +// 导出到全局 +window.EvaluationManager = EvaluationManager; \ No newline at end of file diff --git a/src/flask_prompt_master/templates/comparison_evaluation.html b/src/flask_prompt_master/templates/comparison_evaluation.html new file mode 100644 index 0000000..f3907b7 --- /dev/null +++ b/src/flask_prompt_master/templates/comparison_evaluation.html @@ -0,0 +1,567 @@ + + + + + + 提示词对比评价 - 提示词大师 + + + + + + + + + + + {% extends "base.html" %} + {% block title %}提示词对比评价{% endblock %} + + {% block content %} +
+
+
+
+

提示词对比评价

+ +
+ + +
+
+
+
+
+ +
+
1. 输入原始提示词
+
+
+
+ +
+
2. 选择优化模板
+
+
+
+ +
+
3. 对比优化版本
+
+
+
+ +
+
4. 评价并保存
+
+
+
+
+ + +
+
+
原始提示词
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+
选择优化模板
+
+
+

选择多个优化模板,系统将为每个模板生成一个优化版本

+
+ +
+
+ + +
+
+
+ + +
+
+ +

点击生成后,系统将根据您选择的模板生成多个优化版本

+
+
+ + + + + + + + + +
+
+
+ {% endblock %} + + {% block scripts %} + + {% endblock %} + + \ No newline at end of file diff --git a/src/flask_prompt_master/templates/evaluation_history.html b/src/flask_prompt_master/templates/evaluation_history.html new file mode 100644 index 0000000..b1d90af --- /dev/null +++ b/src/flask_prompt_master/templates/evaluation_history.html @@ -0,0 +1,822 @@ + + + + + + 评价历史 - 提示词大师 + + + + + + + + + + + + + {% extends "base.html" %} + {% block title %}评价历史{% endblock %} + + {% block content %} +
+
+
+
+

评价历史

+ +
+ + +
+
+
+
+
+
+
总评价数
+

0

+
+
+ +
+
+
+
+
+
+
+
+
+
+
平均综合评分
+

0.0

+
+
+ +
+
+
+
+
+
+
+
+
+
+
对比组数量
+

0

+
+
+ +
+
+
+
+
+
+
+
+
+
+
最佳版本数
+

0

+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+
+
+
评分趋势
+
+
+
+ +
+
+
+
+
+
+
+
维度分布
+
+
+
+ +
+
+
+
+
+ + +
+
+
评价记录
+
+
+
+ + + + + + + + + + + + + + +
评价时间原始提示词模板维度评分综合评分操作
+
+ + +
+
+ + +
+
+
+
对比组
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + {% endblock %} + + {% block scripts %} + + {% endblock %} + + \ No newline at end of file diff --git a/vue-app/src/api/modules/comparison.ts b/vue-app/src/api/modules/comparison.ts new file mode 100644 index 0000000..d098d69 --- /dev/null +++ b/vue-app/src/api/modules/comparison.ts @@ -0,0 +1,99 @@ +/** + * 对比组管理API模块 + */ + +import client from '../client' +import type { + ComparisonGroup, + ComparisonGroupCreation +} from '../types/evaluation' + +interface ComparisonGroupsResponse { + success: boolean + message?: string + data: { + groups: ComparisonGroup[] + total: number + } +} + +interface ComparisonGroupDetailResponse { + success: boolean + message?: string + data: { + group: ComparisonGroup + histories: any[] // 实际类型应为 HistoryItem + total_histories: number + evaluations: any[] // 实际类型应为 PromptEvaluation + total_evaluations: number + } +} + +interface AddToGroupResponse { + success: boolean + message?: string + data: { + group_id: string + added_histories: any[] + total_histories: number + } +} + +/** + * 创建对比组 + */ +export function createComparisonGroup(data: ComparisonGroupCreation) { + return client + .post('/api/comparison/create', data) + .then((r) => r.data) +} + +/** + * 获取用户的对比组列表 + */ +export function getUserComparisonGroups(userId?: number) { + const params = userId ? { user_id: userId } : undefined + return client + .get('/api/comparison/groups', { params }) + .then((r) => r.data) +} + +/** + * 获取对比组详情 + */ +export function getComparisonGroupDetail(groupId: string) { + return client + .get(`/api/comparison/${groupId}`) + .then((r) => r.data) +} + +/** + * 向对比组添加提示词 + */ +export function addToComparisonGroup(groupId: string, historyIds: number[]) { + return client + .post(`/api/comparison/${groupId}/add`, { history_ids: historyIds }) + .then((r) => r.data) +} + +/** + * 删除对比组 + */ +export function deleteComparisonGroup(groupId: string) { + return client + .delete(`/api/comparison/${groupId}`) + .then((r) => r.data) +} + +/** + * 更新对比组信息 + */ +export function updateComparisonGroup(groupId: string, data: { + name?: string + description?: string + is_public?: boolean +}) { + return client + .put(`/api/comparison/${groupId}`, data) + .then((r) => r.data) +} \ No newline at end of file diff --git a/vue-app/src/api/modules/evaluation.ts b/vue-app/src/api/modules/evaluation.ts new file mode 100644 index 0000000..a33d0ea --- /dev/null +++ b/vue-app/src/api/modules/evaluation.ts @@ -0,0 +1,90 @@ +/** + * 提示词评价API模块 + */ + +import client from '../client' +import type { + EvaluationSubmission, + EvaluationHistoryResponse, + SubmitEvaluationResponse, + ComparisonGroupCreation, + BatchComparisonResponse, + EvaluationStatisticsResponse, + ComparisonGroupResponse, + EvaluationListResponse, +} from '../types/evaluation' + +/** + * 提交评价 + */ +export function submitEvaluation(data: EvaluationSubmission) { + return client + .post('/api/evaluation/submit', data) + .then((r) => r.data) +} + +/** + * 当前用户评价列表(分页) + */ +export function listEvaluations(params?: { + page?: number + per_page?: number + history_id?: number + days?: number +}) { + return client + .get('/api/evaluation/list', { params }) + .then((r) => r.data) +} + +/** + * 获取历史记录的评价 + */ +export function getHistoryEvaluations(historyId: number) { + return client + .get(`/api/evaluation/history/${historyId}`) + .then((r) => r.data) +} + +/** + * 获取对比组评价 + */ +export function getComparisonEvaluations(groupId: string) { + return client + .get(`/api/evaluation/comparison/${groupId}`) + .then((r) => r.data) +} + +/** + * 批量对比评价 + */ +export function batchCompareEvaluations(data: ComparisonGroupCreation) { + return client + .post('/api/evaluation/compare', data) + .then((r) => r.data) +} + +/** + * 获取评价统计信息 + */ +export function getEvaluationStatistics(params?: { + history_id?: number + user_id?: number + days?: number +}) { + return client + .get('/api/evaluation/stats', { params }) + .then((r) => r.data) +} + +/** + * 标记为最佳版本 + */ +export function markAsBestVersion(evaluationId: number, groupId: string) { + return client + .post('/api/evaluation/best-version', { + evaluation_id: evaluationId, + comparison_group_id: groupId + }) + .then((r) => r.data) +} \ No newline at end of file diff --git a/vue-app/src/api/modules/promptQuality.ts b/vue-app/src/api/modules/promptQuality.ts new file mode 100644 index 0000000..dc00627 --- /dev/null +++ b/vue-app/src/api/modules/promptQuality.ts @@ -0,0 +1,36 @@ +import client from '../client' +import type { + PromptQualityAnalyzeResponse, + PromptQualityHistoryResponse, + PromptQualityBatchDetailResponse, +} from '../types/promptQuality' + +export function analyzePromptQuality(body: { + prompts: string[] + task_context?: string +}) { + return client + .post('/api/prompt-quality/analyze', body) + .then((r) => r.data) +} + +export function fetchPromptQualityHistory(params?: { + page?: number + per_page?: number + q?: string + batch_id?: string + date_from?: string + date_to?: string +}) { + return client + .get('/api/prompt-quality/history', { params }) + .then((r) => r.data) +} + +export function fetchPromptQualityBatch(batchId: string) { + return client + .get( + `/api/prompt-quality/history/batch/${encodeURIComponent(batchId)}`, + ) + .then((r) => r.data) +} diff --git a/vue-app/src/api/types/evaluation.ts b/vue-app/src/api/types/evaluation.ts new file mode 100644 index 0000000..c5f4b7d --- /dev/null +++ b/vue-app/src/api/types/evaluation.ts @@ -0,0 +1,157 @@ +/** + * 提示词评价相关类型定义 + */ + +/** 列表接口会附带历史摘要,便于展示优化后提示词预览 */ +export interface PromptEvaluationHistorySnippet { + id: number + generated_prompt?: string | null + original_input?: string | null +} + +export interface PromptEvaluation { + id: number + history_id: number + user_id: number + clarity_rating: number | null + specificity_rating: number | null + effectiveness_rating: number | null + professionalism_rating: number | null + completeness_rating: number | null + is_best_version: boolean + comparison_group_id: string | null + overall_rating: number | null + comments: string | null + created_at: string | null + updated_at: string | null + average_dimension_rating: number | null + history?: PromptEvaluationHistorySnippet | null +} + +/** 与后端 dimension-analysis / 表单字段一致 */ +export type EvaluationDimensionField = + | 'clarity_rating' + | 'specificity_rating' + | 'effectiveness_rating' + | 'professionalism_rating' + | 'completeness_rating' + +export interface EvaluationSubmission { + history_id: number + clarity_rating?: number | null + specificity_rating?: number | null + effectiveness_rating?: number | null + professionalism_rating?: number | null + completeness_rating?: number | null + is_best_version?: boolean + comparison_group_id?: string | null + overall_rating?: number | null + comments?: string | null +} + +/** 与 Flask PromptEvaluation.get_evaluation_statistics 返回的 statistics 对齐 */ +export interface EvaluationStats { + total_evaluations: number + average_overall_rating: number + dimension_averages: { + clarity: number + specificity: number + effectiveness: number + professionalism: number + completeness: number + } + best_version_count: number +} + +export interface TrendDataPoint { + date: string + total_evaluations: number + average_overall_rating: number +} + +export interface EvaluationHistoryResponse { + success: boolean + message?: string + data: { + history_id: number + evaluations: PromptEvaluation[] + total: number + } +} + +export interface SubmitEvaluationResponse { + success: boolean + message?: string + data: PromptEvaluation +} + +export interface ComparisonEvaluationData { + group_id: string + group_info: ComparisonGroup | null + evaluations: PromptEvaluation[] + best_version: PromptEvaluation | null + histories: any[] // 实际类型应为 HistoryItem,暂时用 any + total_histories: number + total_evaluations: number +} + +export interface ComparisonGroup { + id: number + user_id: number + group_id: string + name: string + description: string | null + is_public: boolean + created_at: string + updated_at: string +} + +export interface ComparisonGroupCreation { + history_ids: number[] + group_name?: string + group_description?: string + is_public?: boolean +} + +export interface BatchComparisonResponse { + success: boolean + message?: string + data: { + group_id: string + group_info: ComparisonGroup + histories: any[] + total_histories: number + } +} + +export interface EvaluationStatisticsResponse { + success: boolean + message?: string + data: { + statistics: EvaluationStats + trend_data: TrendDataPoint[] + time_range: { + date_from: string + date_to: string + days: number + } + } +} + +export interface ComparisonGroupResponse { + success: boolean + message?: string + data: ComparisonEvaluationData +} + +export interface EvaluationListResponse { + success: boolean + message?: string + data: { + evaluations: PromptEvaluation[] + total: number + page: number + per_page: number + pages: number + } +} \ No newline at end of file diff --git a/vue-app/src/api/types/promptQuality.ts b/vue-app/src/api/types/promptQuality.ts new file mode 100644 index 0000000..ecd8dea --- /dev/null +++ b/vue-app/src/api/types/promptQuality.ts @@ -0,0 +1,65 @@ +/** 与 POST /api/prompt-quality/analyze 返回的单条结果一致 */ + +export interface PromptQualityEvaluation { + score: number + strengths: string[] + weaknesses: string[] + suggestions: string[] + applicability: string[] + mandatory_fixes: string[] + optimization_suggestions: string[] +} + +export interface PromptQualityResultItem { + optimized_prompt: string + evaluation: PromptQualityEvaluation + history_id: string + timestamp: string | null +} + +export interface PromptQualityAnalyzeResponse { + success: boolean + message?: string + data?: { + batch_id: string + results: PromptQualityResultItem[] + batch_comparison: string + incremental_learning_hints: string[] + } +} + +export interface PromptQualityHistoryRow { + id: number + batch_id: string + prompt_index: number + preview: string + score: number | undefined + created_at: string | null +} + +export interface PromptQualityHistoryResponse { + success: boolean + message?: string + data?: { + items: PromptQualityHistoryRow[] + pagination: { + page: number + per_page: number + total: number + pages: number + has_next: boolean + has_prev: boolean + } + } +} + +export interface PromptQualityBatchDetailResponse { + success: boolean + message?: string + data?: { + batch_id: string + results: PromptQualityResultItem[] + batch_comparison: string + incremental_learning_hints: string[] + } +} diff --git a/vue-app/src/api/types/template.ts b/vue-app/src/api/types/template.ts index ced68c3..3ebfa5e 100644 --- a/vue-app/src/api/types/template.ts +++ b/vue-app/src/api/types/template.ts @@ -29,6 +29,7 @@ export interface GeneratePromptResponse { success: boolean message?: string generated_text?: string + generation_time?: number prompt?: { id: number input_text: string diff --git a/vue-app/src/components.d.ts b/vue-app/src/components.d.ts index eb2038b..cc279e7 100644 --- a/vue-app/src/components.d.ts +++ b/vue-app/src/components.d.ts @@ -19,6 +19,7 @@ declare module 'vue' { ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] + ElDivider: typeof import('element-plus/es')['ElDivider'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] @@ -34,6 +35,7 @@ declare module 'vue' { ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSpace: typeof import('element-plus/es')['ElSpace'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] diff --git a/vue-app/src/components/layout/AppHeader.vue b/vue-app/src/components/layout/AppHeader.vue index 9877501..15dbd2e 100644 --- a/vue-app/src/components/layout/AppHeader.vue +++ b/vue-app/src/components/layout/AppHeader.vue @@ -36,6 +36,7 @@ type NavRouteName = | 'optimization' | 'android-tools' | 'resume-optimization' + | 'prompt-quality' function go(name: NavRouteName) { router.push({ name }) @@ -64,6 +65,7 @@ function onMoreCommand(cmd: string) { +