feat: AI 生成增强 - 风格/长度/评分/历史/复制
- 后端 prompt 支持风格选择(6种)和字数范围 - 后端 AI 输出改为 JSON 格式,含评分和推荐理由 - GenerateRequest/Response schema 新增 style/length/score/reason 字段 - 前端新增风格选择、字数范围控件 - 前端新增随机选题按钮 - 前端新增复制内容功能 - 前端新增生成历史记录(会话级 20 条) - 结果卡片展示 AI 评分和推荐理由
This commit is contained in:
+85
-15
@@ -1,7 +1,8 @@
|
|||||||
"""智能笑话生成器 API"""
|
"""智能笑话生成器 API"""
|
||||||
|
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -21,17 +22,43 @@ GENERATION_PROMPT = """你是一位幽默大师,专门创作轻松搞笑的短
|
|||||||
要求:
|
要求:
|
||||||
1. 根据场景和关键词创作一条原创笑话
|
1. 根据场景和关键词创作一条原创笑话
|
||||||
2. 笑话要有反转或意外结局
|
2. 笑话要有反转或意外结局
|
||||||
3. 语言简洁,30-150字
|
3. 语言风格:{style}
|
||||||
4. 直接输出笑话内容,不需要解释
|
4. 字数要求:{length_requirement}
|
||||||
|
5. 直接输出笑话内容,不需要解释
|
||||||
|
|
||||||
格式:
|
请严格按照以下 JSON 格式输出,不要加任何额外说明:
|
||||||
标题:xxx
|
{{
|
||||||
内容:xxx
|
"title": "笑话标题",
|
||||||
"""
|
"content": "笑话正文",
|
||||||
|
"score": 8,
|
||||||
|
"reason": "这个笑话巧妙结合了场景和关键词,结尾有反转"
|
||||||
|
}}
|
||||||
|
|
||||||
|
score 是 1-10 的整数评分,reason 是用一句话说明亮点。"""
|
||||||
|
|
||||||
|
STYLE_MAP = {
|
||||||
|
"cold": "冷幽默 / 无厘头",
|
||||||
|
"warm": "温馨幽默 / 暖心搞笑",
|
||||||
|
"twist": "反转 / 神转折",
|
||||||
|
"pun": "谐音梗 / 文字游戏",
|
||||||
|
"sketch": "段子 / 吐槽调侃",
|
||||||
|
"irony": "讽刺幽默 / 黑色幽默",
|
||||||
|
}
|
||||||
|
|
||||||
|
LENGTH_MAP = {
|
||||||
|
"short": "30-80字,非常简短",
|
||||||
|
"medium": "80-150字,正常长度",
|
||||||
|
"long": "150-300字,可以描述一个小场景",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_prompt(scenarios: list[str], keywords: list[str]) -> str:
|
def _build_prompt(
|
||||||
"""构建 AI prompt"""
|
scenarios: list[str],
|
||||||
|
keywords: list[str],
|
||||||
|
style: str = "twist",
|
||||||
|
length: str = "medium",
|
||||||
|
) -> str:
|
||||||
|
"""构建 AI prompt,支持风格和长度控制"""
|
||||||
parts = []
|
parts = []
|
||||||
if scenarios:
|
if scenarios:
|
||||||
parts.append(f"场景:{', '.join(scenarios)}")
|
parts.append(f"场景:{', '.join(scenarios)}")
|
||||||
@@ -39,7 +66,11 @@ def _build_prompt(scenarios: list[str], keywords: list[str]) -> str:
|
|||||||
parts.append(f"关键词:{', '.join(keywords)}")
|
parts.append(f"关键词:{', '.join(keywords)}")
|
||||||
if not parts:
|
if not parts:
|
||||||
parts.append("场景:日常生活的各种趣事(不指定具体场景)")
|
parts.append("场景:日常生活的各种趣事(不指定具体场景)")
|
||||||
return GENERATION_PROMPT.format(context="\n".join(parts))
|
return GENERATION_PROMPT.format(
|
||||||
|
context="\n".join(parts),
|
||||||
|
style=STYLE_MAP.get(style, "反转 / 神转折"),
|
||||||
|
length_requirement=LENGTH_MAP.get(length, "80-150字,正常长度"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=GenerateResponse)
|
@router.post("", response_model=GenerateResponse)
|
||||||
@@ -47,11 +78,10 @@ def generate_joke(
|
|||||||
req: GenerateRequest,
|
req: GenerateRequest,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""调用 AI 生成笑话"""
|
"""调用 AI 生成笑话,支持风格和长度控制"""
|
||||||
# 获取激活的 AI 配置
|
# 获取激活的 AI 配置
|
||||||
setting = db.query(AiSetting).filter(AiSetting.is_active == True).first()
|
setting = db.query(AiSetting).filter(AiSetting.is_active == True).first()
|
||||||
if not setting:
|
if not setting:
|
||||||
# 如果没有配置,尝试返回默认配置
|
|
||||||
setting = db.query(AiSetting).first()
|
setting = db.query(AiSetting).first()
|
||||||
|
|
||||||
if not setting or not setting.api_key:
|
if not setting or not setting.api_key:
|
||||||
@@ -60,22 +90,62 @@ def generate_joke(
|
|||||||
# 调用 AI
|
# 调用 AI
|
||||||
try:
|
try:
|
||||||
client = OpenAI(base_url=setting.api_base, api_key=setting.api_key)
|
client = OpenAI(base_url=setting.api_base, api_key=setting.api_key)
|
||||||
prompt = _build_prompt(req.scenarios, req.keywords)
|
prompt = _build_prompt(req.scenarios, req.keywords, req.style, req.length)
|
||||||
|
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model=setting.model_name,
|
model=setting.model_name,
|
||||||
messages=[{"role": "user", "content": prompt}],
|
messages=[{"role": "user", "content": prompt}],
|
||||||
temperature=setting.temperature,
|
temperature=req.temperature,
|
||||||
max_tokens=setting.max_tokens,
|
max_tokens=setting.max_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
raw = response.choices[0].message.content
|
raw = response.choices[0].message.content
|
||||||
return _parse_response(raw)
|
return _parse_json_response(raw)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise HTTPException(status_code=500, detail="AI 返回格式异常,请重新生成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_json_response(raw: str | None) -> GenerateResponse:
|
||||||
|
"""解析 AI 返回的 JSON 格式"""
|
||||||
|
if not raw or not raw.strip():
|
||||||
|
return GenerateResponse(title="生成的笑话", content="(内容生成失败,请重新生成)", score=0)
|
||||||
|
|
||||||
|
# 尝试从 markdown 代码块中提取 JSON
|
||||||
|
raw = raw.strip()
|
||||||
|
if raw.startswith("```"):
|
||||||
|
lines = raw.split("\n")
|
||||||
|
# 去掉第一行 ```json 和最后一行 ```
|
||||||
|
if len(lines) >= 3:
|
||||||
|
raw = "\n".join(lines[1:-1]).strip()
|
||||||
|
|
||||||
|
# 移除可能的尾部分号
|
||||||
|
if raw.endswith(","):
|
||||||
|
raw = raw[:-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 如果 JSON 解析失败,尝试查找花括号内的内容
|
||||||
|
try:
|
||||||
|
start = raw.index("{")
|
||||||
|
end = raw.rindex("}") + 1
|
||||||
|
raw = raw[start:end]
|
||||||
|
data = json.loads(raw)
|
||||||
|
except (ValueError, json.JSONDecodeError):
|
||||||
|
return _parse_response(raw)
|
||||||
|
|
||||||
|
return GenerateResponse(
|
||||||
|
title=data.get("title", "生成的笑话"),
|
||||||
|
content=data.get("content", ""),
|
||||||
|
score=data.get("score", 0),
|
||||||
|
reason=data.get("reason", ""),
|
||||||
|
created_at=datetime.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_response(raw: str | None) -> GenerateResponse:
|
def _parse_response(raw: str | None) -> GenerateResponse:
|
||||||
"""解析 AI 返回内容,提取标题和内容"""
|
"""解析 AI 返回内容,提取标题和内容"""
|
||||||
# 防御:处理空或 None 输入
|
# 防御:处理空或 None 输入
|
||||||
|
|||||||
@@ -74,12 +74,18 @@ class GenerateRequest(BaseModel):
|
|||||||
"""笑话生成请求"""
|
"""笑话生成请求"""
|
||||||
keywords: list[str] = []
|
keywords: list[str] = []
|
||||||
scenarios: list[str] = []
|
scenarios: list[str] = []
|
||||||
|
style: str = "twist"
|
||||||
|
length: str = "medium"
|
||||||
|
temperature: float = 0.8
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
json_schema_extra = {
|
json_schema_extra = {
|
||||||
"example": {
|
"example": {
|
||||||
"keywords": ["加班"],
|
"keywords": ["加班"],
|
||||||
"scenarios": ["职场"]
|
"scenarios": ["职场"],
|
||||||
|
"style": "twist",
|
||||||
|
"length": "medium",
|
||||||
|
"temperature": 0.8,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +94,8 @@ class GenerateResponse(BaseModel):
|
|||||||
"""笑话生成响应"""
|
"""笑话生成响应"""
|
||||||
title: str
|
title: str
|
||||||
content: str
|
content: str
|
||||||
|
score: int = 0
|
||||||
|
reason: str = ""
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
+10
-3
@@ -4,11 +4,18 @@ import request from './request'
|
|||||||
* 调用 AI 生成笑话
|
* 调用 AI 生成笑话
|
||||||
* @param {string[]} keywords - 关键词列表
|
* @param {string[]} keywords - 关键词列表
|
||||||
* @param {string[]} scenarios - 场景列表
|
* @param {string[]} scenarios - 场景列表
|
||||||
* @returns {Promise<{title: string, content: string, created_at: string}>}
|
* @param {object} opts - 可选参数
|
||||||
|
* @param {string} opts.style - 风格: cold/warm/twist/pun/sketch/irony
|
||||||
|
* @param {string} opts.length - 长度: short/medium/long
|
||||||
|
* @param {number} opts.temperature - 创造力 0-1
|
||||||
|
* @returns {Promise<{title: string, content: string, score: number, reason: string}>}
|
||||||
*/
|
*/
|
||||||
export const generateJoke = (keywords = [], scenarios = []) => {
|
export const generateJoke = (keywords = [], scenarios = [], opts = {}) => {
|
||||||
return request.post('/generate', {
|
return request.post('/generate', {
|
||||||
keywords,
|
keywords,
|
||||||
scenarios
|
scenarios,
|
||||||
|
style: opts.style || 'twist',
|
||||||
|
length: opts.length || 'medium',
|
||||||
|
temperature: opts.temperature ?? 0.8
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+400
-153
@@ -2,100 +2,130 @@
|
|||||||
<div class="generate-page">
|
<div class="generate-page">
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
<span class="emoji">✨</span>
|
<span class="emoji">✨</span>
|
||||||
智能笑话生成器
|
AI 笑话生成
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- 场景选择 -->
|
<!-- 场景选择 -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-label">选择场景(可多选)</div>
|
<div class="section-label">选择场景(可多选)</div>
|
||||||
<div class="scenario-chips">
|
<div class="chips-group">
|
||||||
<el-check-tag
|
<span
|
||||||
v-for="s in predefinedScenarios"
|
v-for="s in predefinedScenarios"
|
||||||
:key="s"
|
:key="s"
|
||||||
:checked="selectedScenarios.includes(s)"
|
class="chip"
|
||||||
@change="toggleScenario(s)"
|
:class="{ checked: selectedScenarios.includes(s) }"
|
||||||
class="scenario-chip"
|
@click="toggleScenario(s)"
|
||||||
>
|
>{{ s }}</span>
|
||||||
{{ s }}
|
|
||||||
</el-check-tag>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 关键词输入 -->
|
<!-- 关键词输入 -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-label">添加关键词(可选)</div>
|
<div class="section-label">添加关键词(可选)</div>
|
||||||
<el-input
|
<div class="input-row">
|
||||||
v-model="keywordInput"
|
<input
|
||||||
placeholder="输入关键词,如:加班、相亲、熊孩子..."
|
v-model="keywordInput"
|
||||||
@keyup.enter="addKeyword"
|
type="text"
|
||||||
clearable
|
placeholder="输入关键词,如:加班、相亲、熊孩子..."
|
||||||
>
|
@keyup.enter="addKeyword"
|
||||||
<template #append>
|
/>
|
||||||
<el-button @click="addKeyword">添加</el-button>
|
<button class="btn-add" @click="addKeyword">添加</button>
|
||||||
</template>
|
</div>
|
||||||
</el-input>
|
<div v-if="keywords.length" class="tag-list">
|
||||||
<div v-if="keywords.length" class="keyword-tags">
|
<span v-for="(kw, i) in keywords" :key="i" class="tag-item">
|
||||||
<el-tag
|
|
||||||
v-for="(kw, i) in keywords"
|
|
||||||
:key="i"
|
|
||||||
closable
|
|
||||||
@close="removeKeyword(i)"
|
|
||||||
class="keyword-tag"
|
|
||||||
>
|
|
||||||
{{ kw }}
|
{{ kw }}
|
||||||
</el-tag>
|
<span class="tag-close" @click="removeKeyword(i)">×</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 风格选择 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-label">笑话风格</div>
|
||||||
|
<div class="chips-group">
|
||||||
|
<span
|
||||||
|
v-for="s in styles"
|
||||||
|
:key="s.value"
|
||||||
|
class="chip style"
|
||||||
|
:class="{ checked: selectedStyle === s.value }"
|
||||||
|
@click="selectedStyle = s.value"
|
||||||
|
>{{ s.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 字数范围 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-label">字数范围</div>
|
||||||
|
<div class="chips-group">
|
||||||
|
<span
|
||||||
|
v-for="l in lengths"
|
||||||
|
:key="l.value"
|
||||||
|
class="chip length"
|
||||||
|
:class="{ checked: selectedLength === l.value }"
|
||||||
|
@click="selectedLength = l.value"
|
||||||
|
>{{ l.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 生成按钮 -->
|
<!-- 生成按钮 -->
|
||||||
<div class="generate-actions">
|
<div class="generate-actions">
|
||||||
<el-button
|
<button class="btn-generate" :disabled="loading" @click="handleGenerate">
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="loading"
|
|
||||||
@click="handleGenerate"
|
|
||||||
class="generate-btn"
|
|
||||||
>
|
|
||||||
<span v-if="!loading">🎲 开始生成</span>
|
<span v-if="!loading">🎲 开始生成</span>
|
||||||
<span v-else>生成中...</span>
|
<span v-else>生成中...</span>
|
||||||
</el-button>
|
</button>
|
||||||
<el-button v-if="generatedJoke" size="large" @click="resetForm">
|
<button class="btn-random" @click="randomPick">🔀 随机选题</button>
|
||||||
清空
|
<button v-if="generatedJoke" class="btn-clear" @click="resetForm">清空</button>
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 生成结果 -->
|
<!-- 生成结果 -->
|
||||||
<div v-if="generatedJoke" class="result-card">
|
<div v-if="generatedJoke" class="result-card">
|
||||||
<h2 class="result-title">{{ generatedJoke.title }}</h2>
|
<div class="result-header">
|
||||||
|
<h2 class="result-title">{{ generatedJoke.title }}</h2>
|
||||||
|
<span v-if="generatedJoke.score" class="result-score">{{ generatedJoke.score }}/10</span>
|
||||||
|
</div>
|
||||||
<pre class="result-content">{{ generatedJoke.content }}</pre>
|
<pre class="result-content">{{ generatedJoke.content }}</pre>
|
||||||
|
<div v-if="generatedJoke.reason" class="result-reason">
|
||||||
|
<span class="reason-icon">💡</span> {{ generatedJoke.reason }}
|
||||||
|
</div>
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<el-button type="primary" @click="handleGenerate">
|
<button class="btn-primary" title="复制到剪贴板" @click="copyContent">📋 复制</button>
|
||||||
🔄 重新生成
|
<button class="btn-primary" @click="handleGenerate">🔄 重新生成</button>
|
||||||
</el-button>
|
<button class="btn-fav" :class="{ favorited: isFavorited }" @click="handleFavorite">
|
||||||
<el-button @click="handleFavorite" :type="isFavorited ? 'danger' : 'default'">
|
|
||||||
<span v-if="!isFavorited">❤ 收藏</span>
|
<span v-if="!isFavorited">❤ 收藏</span>
|
||||||
<span v-else>✔ 已收藏</span>
|
<span v-else>✔ 已收藏</span>
|
||||||
</el-button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 历史记录 -->
|
||||||
|
<div v-if="history.length" class="history-section">
|
||||||
|
<h3>📄 生成历史 ({{ history.length }})</h3>
|
||||||
|
<div class="history-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in history"
|
||||||
|
:key="i"
|
||||||
|
class="history-item"
|
||||||
|
@click="selectHistory(i)"
|
||||||
|
>
|
||||||
|
<div class="history-title">{{ item.title }}</div>
|
||||||
|
<div class="history-meta">
|
||||||
|
<span>{{ item.content.substring(0, 40) }}...</span>
|
||||||
|
<span class="history-del" @click.stop="removeHistory(i)">×</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 收藏列表 -->
|
<!-- 收藏列表 -->
|
||||||
<div v-if="favorites.length" class="favorites-section">
|
<div v-if="favorites.length" class="favorites-section">
|
||||||
<h3>📬 我的收藏 ({{ favorites.length }})</h3>
|
<h3>📬 我的收藏 ({{ favorites.length }})</h3>
|
||||||
<div class="favorite-list">
|
<div class="fav-list">
|
||||||
<div
|
<div v-for="(fav, i) in favorites" :key="fav.id" class="fav-item">
|
||||||
v-for="(fav, i) in favorites"
|
|
||||||
:key="fav.id"
|
|
||||||
class="favorite-item"
|
|
||||||
>
|
|
||||||
<div class="fav-title">{{ fav.title }}</div>
|
<div class="fav-title">{{ fav.title }}</div>
|
||||||
<div class="fav-content">{{ fav.content.substring(0, 100) }}...</div>
|
<div class="fav-content">{{ fav.content.substring(0, 100) }}...</div>
|
||||||
<div class="fav-meta">
|
<div class="fav-meta">
|
||||||
<span class="fav-date">{{ formatDate(fav.created_at) }}</span>
|
<span class="fav-date">{{ formatDate(fav.created_at) }}</span>
|
||||||
<el-button type="danger" size="small" text @click="removeFavorite(i)">
|
<button class="btn-del" @click="removeFavorite(i)">删除</button>
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,31 +134,48 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { generateJoke } from '@/api/generate'
|
import { generateJoke } from '@/api/generate'
|
||||||
|
|
||||||
const predefinedScenarios = ['职场', '校园', '社交', '家庭', '情感', '搞笑日常']
|
const predefinedScenarios = ['职场', '校园', '社交', '家庭', '情感', '搞笑日常']
|
||||||
|
|
||||||
|
const styles = [
|
||||||
|
{ value: 'twist', label: '神反转' },
|
||||||
|
{ value: 'cold', label: '冷幽默' },
|
||||||
|
{ value: 'warm', label: '暖心' },
|
||||||
|
{ value: 'pun', label: '谐音梗' },
|
||||||
|
{ value: 'sketch', label: '吐槽段子' },
|
||||||
|
{ value: 'irony', label: '黑色幽默' },
|
||||||
|
]
|
||||||
|
const lengths = [
|
||||||
|
{ value: 'short', label: '短(30-50字)' },
|
||||||
|
{ value: 'medium', label: '中(50-150字)' },
|
||||||
|
{ value: 'long', label: '长(150-300字)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 输入状态
|
||||||
const selectedScenarios = ref([])
|
const selectedScenarios = ref([])
|
||||||
const keywordInput = ref('')
|
const keywordInput = ref('')
|
||||||
const keywords = ref([])
|
const keywords = ref([])
|
||||||
|
const selectedStyle = ref('twist')
|
||||||
|
const selectedLength = ref('medium')
|
||||||
|
|
||||||
|
// 输出状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const generatedJoke = ref(null)
|
const generatedJoke = ref(null)
|
||||||
const isFavorited = ref(false)
|
const isFavorited = ref(false)
|
||||||
|
|
||||||
|
// 历史记录(会话级)
|
||||||
|
const history = ref([])
|
||||||
|
|
||||||
// 收藏
|
// 收藏
|
||||||
const favorites = ref(JSON.parse(localStorage.getItem('joke_favorites') || '[]'))
|
const favorites = ref(JSON.parse(localStorage.getItem('joke_favorites') || '[]'))
|
||||||
|
|
||||||
const STORAGE_KEY = 'joke_favorites'
|
const STORAGE_KEY = 'joke_favorites'
|
||||||
|
|
||||||
function toggleScenario(s) {
|
function toggleScenario(s) {
|
||||||
const idx = selectedScenarios.value.indexOf(s)
|
const idx = selectedScenarios.value.indexOf(s)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) selectedScenarios.value.splice(idx, 1)
|
||||||
selectedScenarios.value.splice(idx, 1)
|
else selectedScenarios.value.push(s)
|
||||||
} else {
|
|
||||||
selectedScenarios.value.push(s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addKeyword() {
|
function addKeyword() {
|
||||||
@@ -143,61 +190,101 @@ function removeKeyword(i) {
|
|||||||
keywords.value.splice(i, 1)
|
keywords.value.splice(i, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 随机选题
|
||||||
|
function randomPick() {
|
||||||
|
const allScenarios = ['职场', '校园', '社交', '家庭', '情感', '搞笑日常', '医院', '地铁', '餐厅', '健身房']
|
||||||
|
const allStyles = styles.map(s => s.value)
|
||||||
|
selectedScenarios.value = [allScenarios[Math.floor(Math.random() * allScenarios.length)]]
|
||||||
|
selectedStyle.value = allStyles[Math.floor(Math.random() * allStyles.length)]
|
||||||
|
keywords.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成
|
||||||
async function handleGenerate() {
|
async function handleGenerate() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
isFavorited.value = false
|
isFavorited.value = false
|
||||||
try {
|
try {
|
||||||
const res = await generateJoke(keywords.value, selectedScenarios.value)
|
const res = await generateJoke(keywords.value, selectedScenarios.value, {
|
||||||
|
style: selectedStyle.value,
|
||||||
|
length: selectedLength.value,
|
||||||
|
})
|
||||||
generatedJoke.value = res
|
generatedJoke.value = res
|
||||||
// 检查是否已收藏
|
// 加入历史
|
||||||
const exists = favorites.value.find(f => f.content === res.content)
|
history.value.unshift({ ...res })
|
||||||
isFavorited.value = !!exists
|
if (history.value.length > 20) history.value.pop()
|
||||||
|
// 检查收藏状态
|
||||||
|
isFavorited.value = !!favorites.value.find(f => f.content === res.content)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e?.detail || e?.message || '生成失败,请重试'
|
const msg = e?.detail || e?.message || '生成失败,请重试'
|
||||||
ElMessage.error(msg)
|
alert(msg)
|
||||||
generatedJoke.value = null
|
generatedJoke.value = null
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从历史选择
|
||||||
|
function selectHistory(i) {
|
||||||
|
generatedJoke.value = history.value[i]
|
||||||
|
isFavorited.value = !!favorites.value.find(f => f.content === history.value[i].content)
|
||||||
|
// 移到最前
|
||||||
|
const item = history.value.splice(i, 1)[0]
|
||||||
|
history.value.unshift(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制
|
||||||
|
function copyContent() {
|
||||||
|
if (!generatedJoke.value) return
|
||||||
|
const text = `${generatedJoke.value.title}\n\n${generatedJoke.value.content}`
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
alert('已复制到剪贴板')
|
||||||
|
}).catch(() => {
|
||||||
|
// fallback
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
alert('已复制到剪贴板')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeHistory(i) {
|
||||||
|
history.value.splice(i, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收藏
|
||||||
function handleFavorite() {
|
function handleFavorite() {
|
||||||
if (!generatedJoke.value) return
|
if (!generatedJoke.value) return
|
||||||
// 检查是否已存在
|
if (favorites.value.find(f => f.content === generatedJoke.value.content)) {
|
||||||
const exists = favorites.value.findIndex(f => f.content === generatedJoke.value.content)
|
alert('已经收藏过了')
|
||||||
if (exists >= 0) {
|
|
||||||
ElMessage.info('已经收藏过了')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newFavorite = {
|
const newFav = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
title: generatedJoke.value.title,
|
title: generatedJoke.value.title,
|
||||||
content: generatedJoke.value.content,
|
content: generatedJoke.value.content,
|
||||||
|
score: generatedJoke.value.score,
|
||||||
|
reason: generatedJoke.value.reason,
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
}
|
}
|
||||||
favorites.value.unshift(newFavorite)
|
favorites.value.unshift(newFav)
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites.value))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites.value))
|
||||||
isFavorited.value = true
|
isFavorited.value = true
|
||||||
ElMessage.success('收藏成功')
|
alert('收藏成功')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 处理 localStorage 满的情况
|
favorites.value.shift()
|
||||||
favorites.value.shift() // 移除刚添加的
|
alert('收藏失败,存储空间可能已满')
|
||||||
ElMessage.error('收藏失败,存储空间可能已满,请清理部分收藏')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFavorite(i) {
|
function removeFavorite(i) {
|
||||||
favorites.value.splice(i, 1)
|
favorites.value.splice(i, 1)
|
||||||
try {
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites.value))
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites.value))
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error('保存失败')
|
|
||||||
}
|
|
||||||
// 检查当前生成的是否也从收藏中被删除了
|
|
||||||
if (generatedJoke.value) {
|
if (generatedJoke.value) {
|
||||||
const stillExists = favorites.value.find(f => f.content === generatedJoke.value.content)
|
isFavorited.value = !!favorites.value.find(f => f.content === generatedJoke.value.content)
|
||||||
isFavorited.value = !!stillExists
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,152 +303,312 @@ function formatDate(dateStr) {
|
|||||||
.generate-page {
|
.generate-page {
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 40px 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 28px;
|
font-size: 26px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 32px;
|
||||||
}
|
|
||||||
.emoji {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
.emoji { margin-right: 8px; }
|
||||||
|
|
||||||
.section {
|
.section { margin-bottom: 22px; }
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
.section-label {
|
.section-label {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-chips {
|
/* 标签组(场景 + 风格 + 长度 共用) */
|
||||||
|
.chips-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
:deep(.scenario-chip) {
|
.chip {
|
||||||
padding: 8px 18px;
|
padding: 7px 16px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1.5px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
:deep(.scenario-chip:hover) {
|
.chip:hover { border-color: #9333ea; }
|
||||||
border-color: #9333ea;
|
.chip.checked {
|
||||||
}
|
|
||||||
:deep(.scenario-chip.is-checked) {
|
|
||||||
background: #9333ea;
|
background: #9333ea;
|
||||||
border-color: #9333ea;
|
border-color: #9333ea;
|
||||||
color: white;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
/* 风格标签紫色,长度标签用橙色 */
|
||||||
|
.chip.style.checked { background: #7c3aed; border-color: #7c3aed; }
|
||||||
|
.chip.length.checked { background: var(--primary); border-color: var(--primary); }
|
||||||
|
|
||||||
.keyword-tags {
|
/* 输入行 */
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.input-row input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.input-row input:focus { outline: none; border-color: var(--primary); }
|
||||||
|
.input-row input::placeholder { color: var(--text-muted); }
|
||||||
|
.btn-add {
|
||||||
|
padding: 9px 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.btn-add:hover { border-color: var(--primary); color: var(--primary); }
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.keyword-tag {
|
.tag-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
background: #f3e8ff;
|
background: #f3e8ff;
|
||||||
border-color: #9333ea;
|
border: 1px solid #9333ea;
|
||||||
|
border-radius: 14px;
|
||||||
color: #9333ea;
|
color: #9333ea;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.tag-close { cursor: pointer; font-size: 16px; line-height: 1; opacity: 0.7; }
|
||||||
|
.tag-close:hover { opacity: 1; }
|
||||||
|
|
||||||
|
/* 按钮区 */
|
||||||
.generate-actions {
|
.generate-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 28px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
:deep(.generate-btn) {
|
.btn-generate {
|
||||||
padding: 12px 40px;
|
padding: 12px 40px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: linear-gradient(135deg, #9333ea, #7c3aed);
|
background: linear-gradient(135deg, #9333ea, #7c3aed);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
:deep(.generate-btn:hover) {
|
.btn-generate:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(147,51,234,0.4); }
|
||||||
box-shadow: 0 4px 20px rgba(147, 51, 234, 0.4);
|
.btn-generate:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
.btn-random {
|
||||||
|
padding: 12px 22px;
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
.btn-random:hover { border-color: #9333ea; color: #9333ea; }
|
||||||
|
.btn-clear {
|
||||||
|
padding: 12px 22px;
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-clear:hover { border-color: var(--primary); color: var(--primary); }
|
||||||
|
|
||||||
|
/* 结果卡片 */
|
||||||
.result-card {
|
.result-card {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 32px;
|
padding: 28px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 24px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
.result-title {
|
.result-title {
|
||||||
font-size: 22px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: 16px;
|
margin: 0;
|
||||||
text-align: center;
|
}
|
||||||
|
.result-score {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #9333ea;
|
||||||
|
background: #f3e8ff;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.result-content {
|
.result-content {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
line-height: 1.9;
|
line-height: 1.9;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 14px;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
.result-reason {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-page);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.reason-icon { margin-right: 4px; }
|
||||||
.result-actions {
|
.result-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.btn-primary {
|
||||||
|
padding: 9px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: var(--primary);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { opacity: 0.9; }
|
||||||
|
.btn-fav {
|
||||||
|
padding: 9px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.btn-fav:hover { border-color: #e53935; color: #e53935; }
|
||||||
|
.btn-fav.favorited { border-color: #e53935; color: #e53935; background: #fff0f0; }
|
||||||
|
|
||||||
.favorites-section {
|
/* 历史记录 */
|
||||||
margin-top: 40px;
|
.history-section { margin-top: 32px; }
|
||||||
}
|
.history-section h3 {
|
||||||
.favorites-section h3 {
|
font-size: 17px;
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
.history-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.favorite-list {
|
.history-item {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.favorite-item {
|
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 16px 20px;
|
padding: 12px 16px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
.fav-title {
|
.history-item:hover { border-color: var(--primary); }
|
||||||
font-weight: 600;
|
.history-title { font-weight: 600; font-size: 14px; color: var(--text-primary); margin-bottom: 2px; }
|
||||||
color: var(--text-primary);
|
.history-meta {
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.fav-content {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.fav-meta {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
.fav-date {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
.history-del {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.history-del:hover { opacity: 1; color: #e53935; }
|
||||||
|
|
||||||
|
/* 收藏 */
|
||||||
|
.favorites-section { margin-top: 32px; }
|
||||||
|
.favorites-section h3 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.fav-list { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.fav-item {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 14px 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.fav-title { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
|
||||||
|
.fav-content { font-size: 13px; color: var(--text-muted); margin-bottom: 6px; line-height: 1.5; }
|
||||||
|
.fav-meta { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.fav-date { font-size: 12px; color: var(--text-muted); }
|
||||||
|
.btn-del {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #e53935;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.btn-del:hover { background: #fff0f0; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
移动端适配
|
||||||
|
============================================================ */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.generate-page { padding: 0; }
|
||||||
|
.page-title { font-size: 22px; margin-bottom: 24px; }
|
||||||
|
.result-card { padding: 20px; }
|
||||||
|
.result-title { font-size: 18px; }
|
||||||
|
.result-content { font-size: 14px; }
|
||||||
|
.btn-generate { width: 100%; padding: 12px 20px; }
|
||||||
|
.btn-random { flex: 1; text-align: center; }
|
||||||
|
.input-row { flex-direction: column; }
|
||||||
|
.chips-group { gap: 6px; }
|
||||||
|
.chip { font-size: 13px; padding: 6px 14px; }
|
||||||
|
.generate-actions { flex-direction: column; }
|
||||||
|
.btn-clear { width: 100%; text-align: center; }
|
||||||
|
.result-header { flex-direction: column; align-items: flex-start; gap: 6px; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user