Showing
4 changed files
with
374 additions
and
13 deletions
| @@ -33,6 +33,7 @@ ALLOWED_BLOCK_TYPES: List[str] = [ | @@ -33,6 +33,7 @@ ALLOWED_BLOCK_TYPES: List[str] = [ | ||
| 33 | "paragraph", | 33 | "paragraph", |
| 34 | "list", | 34 | "list", |
| 35 | "table", | 35 | "table", |
| 36 | + "swotTable", | ||
| 36 | "blockquote", | 37 | "blockquote", |
| 37 | "engineQuote", | 38 | "engineQuote", |
| 38 | "hr", | 39 | "hr", |
| @@ -169,6 +170,63 @@ table_block: Dict[str, Any] = { | @@ -169,6 +170,63 @@ table_block: Dict[str, Any] = { | ||
| 169 | "additionalProperties": True, | 170 | "additionalProperties": True, |
| 170 | } | 171 | } |
| 171 | 172 | ||
| 173 | +swot_item_schema: Dict[str, Any] = { | ||
| 174 | + "title": "SwotItem", | ||
| 175 | + "oneOf": [ | ||
| 176 | + {"type": "string"}, | ||
| 177 | + { | ||
| 178 | + "type": "object", | ||
| 179 | + "properties": { | ||
| 180 | + "title": {"type": "string"}, | ||
| 181 | + "label": {"type": "string"}, | ||
| 182 | + "text": {"type": "string"}, | ||
| 183 | + "detail": {"type": "string"}, | ||
| 184 | + "description": {"type": "string"}, | ||
| 185 | + "evidence": {"type": "string"}, | ||
| 186 | + "impact": {"type": "string"}, | ||
| 187 | + "score": {"type": ["number", "string"]}, | ||
| 188 | + "priority": {"type": ["string", "number"]}, | ||
| 189 | + }, | ||
| 190 | + "required": [], | ||
| 191 | + "additionalProperties": True, | ||
| 192 | + }, | ||
| 193 | + ], | ||
| 194 | +} | ||
| 195 | + | ||
| 196 | +swot_block: Dict[str, Any] = { | ||
| 197 | + "title": "SwotTableBlock", | ||
| 198 | + "type": "object", | ||
| 199 | + "properties": { | ||
| 200 | + "type": {"const": "swotTable"}, | ||
| 201 | + "title": {"type": "string"}, | ||
| 202 | + "summary": {"type": "string"}, | ||
| 203 | + "strengths": { | ||
| 204 | + "type": "array", | ||
| 205 | + "items": {"$ref": "#/definitions/swotItem"}, | ||
| 206 | + }, | ||
| 207 | + "weaknesses": { | ||
| 208 | + "type": "array", | ||
| 209 | + "items": {"$ref": "#/definitions/swotItem"}, | ||
| 210 | + }, | ||
| 211 | + "opportunities": { | ||
| 212 | + "type": "array", | ||
| 213 | + "items": {"$ref": "#/definitions/swotItem"}, | ||
| 214 | + }, | ||
| 215 | + "threats": { | ||
| 216 | + "type": "array", | ||
| 217 | + "items": {"$ref": "#/definitions/swotItem"}, | ||
| 218 | + }, | ||
| 219 | + }, | ||
| 220 | + "required": ["type"], | ||
| 221 | + "anyOf": [ | ||
| 222 | + {"required": ["strengths"]}, | ||
| 223 | + {"required": ["weaknesses"]}, | ||
| 224 | + {"required": ["opportunities"]}, | ||
| 225 | + {"required": ["threats"]}, | ||
| 226 | + ], | ||
| 227 | + "additionalProperties": True, | ||
| 228 | +} | ||
| 229 | + | ||
| 172 | blockquote_block: Dict[str, Any] = { | 230 | blockquote_block: Dict[str, Any] = { |
| 173 | "title": "BlockquoteBlock", | 231 | "title": "BlockquoteBlock", |
| 174 | "type": "object", | 232 | "type": "object", |
| @@ -361,6 +419,7 @@ block_variants: List[Dict[str, Any]] = [ | @@ -361,6 +419,7 @@ block_variants: List[Dict[str, Any]] = [ | ||
| 361 | kpi_block, | 419 | kpi_block, |
| 362 | widget_block, | 420 | widget_block, |
| 363 | toc_block, | 421 | toc_block, |
| 422 | + swot_block, | ||
| 364 | ] | 423 | ] |
| 365 | 424 | ||
| 366 | CHAPTER_JSON_SCHEMA: Dict[str, Any] = { | 425 | CHAPTER_JSON_SCHEMA: Dict[str, Any] = { |
| @@ -388,6 +447,7 @@ CHAPTER_JSON_SCHEMA: Dict[str, Any] = { | @@ -388,6 +447,7 @@ CHAPTER_JSON_SCHEMA: Dict[str, Any] = { | ||
| 388 | "definitions": { | 447 | "definitions": { |
| 389 | "inlineMark": inline_mark_schema, | 448 | "inlineMark": inline_mark_schema, |
| 390 | "inlineRun": inline_run_schema, | 449 | "inlineRun": inline_run_schema, |
| 450 | + "swotItem": swot_item_schema, | ||
| 391 | "block": {"oneOf": block_variants}, | 451 | "block": {"oneOf": block_variants}, |
| 392 | }, | 452 | }, |
| 393 | } | 453 | } |
| @@ -132,6 +132,39 @@ class IRValidator: | @@ -132,6 +132,39 @@ class IRValidator: | ||
| 132 | errors, | 132 | errors, |
| 133 | ) | 133 | ) |
| 134 | 134 | ||
| 135 | + def _validate_swotTable_block(self, block: Dict[str, Any], path: str, errors: List[str]): | ||
| 136 | + """SWOT表至少提供四象限之一,每象限为条目数组""" | ||
| 137 | + quadrants = ("strengths", "weaknesses", "opportunities", "threats") | ||
| 138 | + if not any(block.get(name) is not None for name in quadrants): | ||
| 139 | + errors.append(f"{path} 需要至少包含 strengths/weaknesses/opportunities/threats 之一") | ||
| 140 | + for name in quadrants: | ||
| 141 | + entries = block.get(name) | ||
| 142 | + if entries is None: | ||
| 143 | + continue | ||
| 144 | + if not isinstance(entries, list): | ||
| 145 | + errors.append(f"{path}.{name} 必须是数组") | ||
| 146 | + continue | ||
| 147 | + for idx, entry in enumerate(entries): | ||
| 148 | + self._validate_swot_item(entry, f"{path}.{name}[{idx}]", errors) | ||
| 149 | + | ||
| 150 | + def _validate_swot_item(self, item: Any, path: str, errors: List[str]): | ||
| 151 | + """单个SWOT条目支持字符串或带字段的对象""" | ||
| 152 | + if isinstance(item, str): | ||
| 153 | + if not item.strip(): | ||
| 154 | + errors.append(f"{path} 不能为空字符串") | ||
| 155 | + return | ||
| 156 | + if not isinstance(item, dict): | ||
| 157 | + errors.append(f"{path} 必须是字符串或对象") | ||
| 158 | + return | ||
| 159 | + title = None | ||
| 160 | + for key in ("title", "label", "text", "detail", "description"): | ||
| 161 | + value = item.get(key) | ||
| 162 | + if isinstance(value, str) and value.strip(): | ||
| 163 | + title = value | ||
| 164 | + break | ||
| 165 | + if title is None: | ||
| 166 | + errors.append(f"{path} 缺少 title/label/text/description 等文字字段") | ||
| 167 | + | ||
| 135 | def _validate_blockquote_block( | 168 | def _validate_blockquote_block( |
| 136 | self, block: Dict[str, Any], path: str, errors: List[str] | 169 | self, block: Dict[str, Any], path: str, errors: List[str] |
| 137 | ): | 170 | ): |
| @@ -304,19 +304,20 @@ SYSTEM_PROMPT_CHAPTER_JSON = f""" | @@ -304,19 +304,20 @@ SYSTEM_PROMPT_CHAPTER_JSON = f""" | ||
| 304 | 3. 所有段落都放入paragraph.inlines,混排样式通过marks表示(bold/italic/color/link等)。 | 304 | 3. 所有段落都放入paragraph.inlines,混排样式通过marks表示(bold/italic/color/link等)。 |
| 305 | 4. 所有heading必须包含anchor,锚点与编号保持模板一致,比如section-2-1。 | 305 | 4. 所有heading必须包含anchor,锚点与编号保持模板一致,比如section-2-1。 |
| 306 | 5. 表格需给出rows/cells/align,KPI卡请使用kpiGrid,分割线用hr。 | 306 | 5. 表格需给出rows/cells/align,KPI卡请使用kpiGrid,分割线用hr。 |
| 307 | -6. 如需引用图表/交互组件,统一用widgetType表示(例如chart.js/line、chart.js/doughnut)。 | ||
| 308 | -7. 鼓励结合outline中列出的子标题,生成多层heading与细粒度内容,同时可补充callout、blockquote等。 | ||
| 309 | -8. engineQuote 仅用于呈现单Agent的原话:使用 block.type="engineQuote",engine 取值 insight/media/query,title 必须固定为对应Agent名字(insight->Insight Agent,media->Media Agent,query->Query Agent,不可自定义),内部 blocks 只允许 paragraph,paragraph.inlines 的 marks 仅可使用 bold/italic(可留空),禁止在 engineQuote 中放表格/图表/引用/公式等;当 reports 或 forumLogs 中有明确的文字段落、结论、数字/时间等可直接引用时,优先分别从 Query/Media/Insight 三个 Agent 摘出关键原文或文字版数据放入 engineQuote,尽量覆盖三类 Agent 而非只用单一来源,严禁臆造内容或把表格/图表改写进 engineQuote。 | ||
| 310 | -9. 如果chapterPlan中包含target/min/max或sections细分预算,请尽量贴合,必要时在notes允许的范围内突破,同时在结构上体现详略; | ||
| 311 | -10. 一级标题需使用中文数字(“一、二、三”),二级标题使用阿拉伯数字(“1.1、1.2”),heading.text中直接写好编号,与outline顺序对应; | ||
| 312 | -11. 严禁输出外部图片/AI生图链接,仅可使用Chart.js图表、表格、色块、callout等HTML原生组件;如需视觉辅助请改为文字描述或数据表; | ||
| 313 | -12. 段落混排需通过marks表达粗体、斜体、下划线、颜色等样式,禁止残留Markdown语法(如**text**); | ||
| 314 | -13. 行间公式用block.type="math"并填入math.latex,行内公式在paragraph.inlines里将文本设为Latex并加上marks.type="math",渲染层会用MathJax处理; | ||
| 315 | -14. widget配色需与CSS变量兼容,不要硬编码背景色或文字色,legend/ticks由渲染层控制; | ||
| 316 | -15. 善用callout、kpiGrid、表格、widget等提升版面丰富度,但必须遵守模板章节范围。 | ||
| 317 | -16. 输出前务必自检JSON语法:禁止出现`{{}}{{`或`][`相连缺少逗号、列表项嵌套超过一层、未闭合的括号或未转义换行,`list` block的items必须是`[[block,...], ...]`结构,若无法满足则返回错误提示而不是输出不合法JSON。 | ||
| 318 | -17. 所有widget块必须在顶层提供`data`或`dataRef`(可将props中的`data`上移),确保Chart.js能够直接渲染;缺失数据时宁可输出表格或段落,绝不留空。 | ||
| 319 | -18. 任何block都必须声明合法`type`(heading/paragraph/list/...);若需要普通文本请使用`paragraph`并给出`inlines`,禁止返回`type:null`或未知值。 | 307 | +6. SWOT分析必须优先使用 block.type="swotTable":分别填写 strengths/weaknesses/opportunities/threats 数组,单项至少包含 title/label/text 之一,可附加 detail/evidence/impact/score 字段;title/summary 字段用于概览说明。 |
| 308 | +7. 如需引用图表/交互组件,统一用widgetType表示(例如chart.js/line、chart.js/doughnut)。 | ||
| 309 | +8. 鼓励结合outline中列出的子标题,生成多层heading与细粒度内容,同时可补充callout、blockquote等。 | ||
| 310 | +9. engineQuote 仅用于呈现单Agent的原话:使用 block.type="engineQuote",engine 取值 insight/media/query,title 必须固定为对应Agent名字(insight->Insight Agent,media->Media Agent,query->Query Agent,不可自定义),内部 blocks 只允许 paragraph,paragraph.inlines 的 marks 仅可使用 bold/italic(可留空),禁止在 engineQuote 中放表格/图表/引用/公式等;当 reports 或 forumLogs 中有明确的文字段落、结论、数字/时间等可直接引用时,优先分别从 Query/Media/Insight 三个 Agent 摘出关键原文或文字版数据放入 engineQuote,尽量覆盖三类 Agent 而非只用单一来源,严禁臆造内容或把表格/图表改写进 engineQuote。 | ||
| 311 | +10. 如果chapterPlan中包含target/min/max或sections细分预算,请尽量贴合,必要时在notes允许的范围内突破,同时在结构上体现详略; | ||
| 312 | +11. 一级标题需使用中文数字(“一、二、三”),二级标题使用阿拉伯数字(“1.1、1.2”),heading.text中直接写好编号,与outline顺序对应; | ||
| 313 | +12. 严禁输出外部图片/AI生图链接,仅可使用Chart.js图表、表格、色块、callout等HTML原生组件;如需视觉辅助请改为文字描述或数据表; | ||
| 314 | +13. 段落混排需通过marks表达粗体、斜体、下划线、颜色等样式,禁止残留Markdown语法(如**text**); | ||
| 315 | +14. 行间公式用block.type="math"并填入math.latex,行内公式在paragraph.inlines里将文本设为Latex并加上marks.type="math",渲染层会用MathJax处理; | ||
| 316 | +15. widget配色需与CSS变量兼容,不要硬编码背景色或文字色,legend/ticks由渲染层控制; | ||
| 317 | +16. 善用callout、kpiGrid、表格、widget等提升版面丰富度,但必须遵守模板章节范围。 | ||
| 318 | +17. 输出前务必自检JSON语法:禁止出现`{{}}{{`或`][`相连缺少逗号、列表项嵌套超过一层、未闭合的括号或未转义换行,`list` block的items必须是`[[block,...], ...]`结构,若无法满足则返回错误提示而不是输出不合法JSON。 | ||
| 319 | +18. 所有widget块必须在顶层提供`data`或`dataRef`(可将props中的`data`上移),确保Chart.js能够直接渲染;缺失数据时宁可输出表格或段落,绝不留空。 | ||
| 320 | +19. 任何block都必须声明合法`type`(heading/paragraph/list/...);若需要普通文本请使用`paragraph`并给出`inlines`,禁止返回`type:null`或未知值。 | ||
| 320 | 321 | ||
| 321 | <CHAPTER JSON SCHEMA> | 322 | <CHAPTER JSON SCHEMA> |
| 322 | {CHAPTER_JSON_SCHEMA_TEXT} | 323 | {CHAPTER_JSON_SCHEMA_TEXT} |
| @@ -48,6 +48,7 @@ class HTMLRenderer: | @@ -48,6 +48,7 @@ class HTMLRenderer: | ||
| 48 | "math", | 48 | "math", |
| 49 | "figure", | 49 | "figure", |
| 50 | "kpiGrid", | 50 | "kpiGrid", |
| 51 | + "swotTable", | ||
| 51 | "engineQuote", | 52 | "engineQuote", |
| 52 | } | 53 | } |
| 53 | INLINE_ARTIFACT_KEYS = { | 54 | INLINE_ARTIFACT_KEYS = { |
| @@ -1021,6 +1022,7 @@ class HTMLRenderer: | @@ -1021,6 +1022,7 @@ class HTMLRenderer: | ||
| 1021 | "paragraph": self._render_paragraph, | 1022 | "paragraph": self._render_paragraph, |
| 1022 | "list": self._render_list, | 1023 | "list": self._render_list, |
| 1023 | "table": self._render_table, | 1024 | "table": self._render_table, |
| 1025 | + "swotTable": self._render_swot_table, | ||
| 1024 | "blockquote": self._render_blockquote, | 1026 | "blockquote": self._render_blockquote, |
| 1025 | "engineQuote": self._render_engine_quote, | 1027 | "engineQuote": self._render_engine_quote, |
| 1026 | "hr": lambda b: "<hr />", | 1028 | "hr": lambda b: "<hr />", |
| @@ -1173,6 +1175,117 @@ class HTMLRenderer: | @@ -1173,6 +1175,117 @@ class HTMLRenderer: | ||
| 1173 | caption_html = f"<caption>{self._escape_html(caption)}</caption>" if caption else "" | 1175 | caption_html = f"<caption>{self._escape_html(caption)}</caption>" if caption else "" |
| 1174 | return f'<div class="table-wrap"><table>{caption_html}<tbody>{rows_html}</tbody></table></div>' | 1176 | return f'<div class="table-wrap"><table>{caption_html}<tbody>{rows_html}</tbody></table></div>' |
| 1175 | 1177 | ||
| 1178 | + def _render_swot_table(self, block: Dict[str, Any]) -> str: | ||
| 1179 | + """ | ||
| 1180 | + 渲染四象限的SWOT专用表格,兼顾HTML与PDF的可读性。 | ||
| 1181 | + """ | ||
| 1182 | + title = block.get("title") or "SWOT 分析" | ||
| 1183 | + summary = block.get("summary") | ||
| 1184 | + quadrants = [ | ||
| 1185 | + ("strengths", "优势 Strengths", "S", "strength"), | ||
| 1186 | + ("weaknesses", "劣势 Weaknesses", "W", "weakness"), | ||
| 1187 | + ("opportunities", "机会 Opportunities", "O", "opportunity"), | ||
| 1188 | + ("threats", "威胁 Threats", "T", "threat"), | ||
| 1189 | + ] | ||
| 1190 | + cells_html = "" | ||
| 1191 | + for key, label, code, css in quadrants: | ||
| 1192 | + items = self._normalize_swot_items(block.get(key)) | ||
| 1193 | + caption_text = f"{len(items)} 条要点" if items else "待补充" | ||
| 1194 | + list_html = "".join(self._render_swot_item(item) for item in items) if items else '<li class="swot-empty">尚未填入要点</li>' | ||
| 1195 | + cells_html += f""" | ||
| 1196 | + <div class="swot-cell {css}"> | ||
| 1197 | + <div class="swot-cell__meta"> | ||
| 1198 | + <span class="swot-pill {css}">{self._escape_html(code)}</span> | ||
| 1199 | + <div> | ||
| 1200 | + <div class="swot-cell__title">{self._escape_html(label)}</div> | ||
| 1201 | + <div class="swot-cell__caption">{self._escape_html(caption_text)}</div> | ||
| 1202 | + </div> | ||
| 1203 | + </div> | ||
| 1204 | + <ul class="swot-list">{list_html}</ul> | ||
| 1205 | + </div>""" | ||
| 1206 | + summary_html = f'<p class="swot-card__summary">{self._escape_html(summary)}</p>' if summary else "" | ||
| 1207 | + title_html = f'<div class="swot-card__title">{self._escape_html(title)}</div>' if title else "" | ||
| 1208 | + legend = """ | ||
| 1209 | + <div class="swot-legend"> | ||
| 1210 | + <span class="swot-legend__item strength">S 优势</span> | ||
| 1211 | + <span class="swot-legend__item weakness">W 劣势</span> | ||
| 1212 | + <span class="swot-legend__item opportunity">O 机会</span> | ||
| 1213 | + <span class="swot-legend__item threat">T 威胁</span> | ||
| 1214 | + </div> | ||
| 1215 | + """ | ||
| 1216 | + return f""" | ||
| 1217 | + <div class="swot-card"> | ||
| 1218 | + <div class="swot-card__head"> | ||
| 1219 | + <div>{title_html}{summary_html}</div> | ||
| 1220 | + {legend} | ||
| 1221 | + </div> | ||
| 1222 | + <div class="swot-grid">{cells_html}</div> | ||
| 1223 | + </div> | ||
| 1224 | + """ | ||
| 1225 | + | ||
| 1226 | + def _normalize_swot_items(self, raw: Any) -> List[Dict[str, Any]]: | ||
| 1227 | + """将SWOT条目规整为统一结构,兼容字符串/对象两种写法""" | ||
| 1228 | + normalized: List[Dict[str, Any]] = [] | ||
| 1229 | + if raw is None: | ||
| 1230 | + return normalized | ||
| 1231 | + if isinstance(raw, (str, int, float)): | ||
| 1232 | + text = self._safe_text(raw).strip() | ||
| 1233 | + if text: | ||
| 1234 | + normalized.append({"title": text}) | ||
| 1235 | + return normalized | ||
| 1236 | + if not isinstance(raw, list): | ||
| 1237 | + return normalized | ||
| 1238 | + for entry in raw: | ||
| 1239 | + if isinstance(entry, (str, int, float)): | ||
| 1240 | + text = self._safe_text(entry).strip() | ||
| 1241 | + if text: | ||
| 1242 | + normalized.append({"title": text}) | ||
| 1243 | + continue | ||
| 1244 | + if not isinstance(entry, dict): | ||
| 1245 | + continue | ||
| 1246 | + title = entry.get("title") or entry.get("label") or entry.get("text") | ||
| 1247 | + detail = entry.get("detail") or entry.get("description") | ||
| 1248 | + evidence = entry.get("evidence") or entry.get("source") | ||
| 1249 | + impact = entry.get("impact") or entry.get("priority") | ||
| 1250 | + score = entry.get("score") | ||
| 1251 | + if not title and isinstance(detail, str): | ||
| 1252 | + title = detail | ||
| 1253 | + detail = None | ||
| 1254 | + if not (title or detail or evidence): | ||
| 1255 | + continue | ||
| 1256 | + normalized.append( | ||
| 1257 | + { | ||
| 1258 | + "title": title, | ||
| 1259 | + "detail": detail, | ||
| 1260 | + "evidence": evidence, | ||
| 1261 | + "impact": impact, | ||
| 1262 | + "score": score, | ||
| 1263 | + } | ||
| 1264 | + ) | ||
| 1265 | + return normalized | ||
| 1266 | + | ||
| 1267 | + def _render_swot_item(self, item: Dict[str, Any]) -> str: | ||
| 1268 | + """输出单个SWOT条目的HTML片段""" | ||
| 1269 | + title = item.get("title") or item.get("label") or item.get("text") or "未命名要点" | ||
| 1270 | + detail = item.get("detail") or item.get("description") | ||
| 1271 | + evidence = item.get("evidence") or item.get("source") | ||
| 1272 | + impact = item.get("impact") or item.get("priority") | ||
| 1273 | + score = item.get("score") | ||
| 1274 | + tags: List[str] = [] | ||
| 1275 | + if impact: | ||
| 1276 | + tags.append(f'<span class="swot-tag">{self._escape_html(impact)}</span>') | ||
| 1277 | + if score not in (None, ""): | ||
| 1278 | + tags.append(f'<span class="swot-tag neutral">评分 {self._escape_html(score)}</span>') | ||
| 1279 | + tags_html = f'<span class="swot-item-tags">{"".join(tags)}</span>' if tags else "" | ||
| 1280 | + detail_html = f'<div class="swot-item-desc">{self._escape_html(detail)}</div>' if detail else "" | ||
| 1281 | + evidence_html = f'<div class="swot-item-evidence">佐证:{self._escape_html(evidence)}</div>' if evidence else "" | ||
| 1282 | + return f""" | ||
| 1283 | + <li class="swot-item"> | ||
| 1284 | + <div class="swot-item-title">{self._escape_html(title)}{tags_html}</div> | ||
| 1285 | + {detail_html}{evidence_html} | ||
| 1286 | + </li> | ||
| 1287 | + """ | ||
| 1288 | + | ||
| 1176 | def _normalize_table_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: | 1289 | def _normalize_table_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: |
| 1177 | """ | 1290 | """ |
| 1178 | 检测并修正仅有单列的竖排表,转换为标准网格。 | 1291 | 检测并修正仅有单列的竖排表,转换为标准网格。 |
| @@ -2446,6 +2559,10 @@ class HTMLRenderer: | @@ -2446,6 +2559,10 @@ class HTMLRenderer: | ||
| 2446 | --engine-query-border: rgba(141, 215, 165, 0.45); | 2559 | --engine-query-border: rgba(141, 215, 165, 0.45); |
| 2447 | --engine-query-text: #a7e2ba; | 2560 | --engine-query-text: #a7e2ba; |
| 2448 | --engine-quote-shadow: 0 12px 28px rgba(0, 0, 0, 0.35); | 2561 | --engine-quote-shadow: 0 12px 28px rgba(0, 0, 0, 0.35); |
| 2562 | + --swot-strength: #1c7f6e; | ||
| 2563 | + --swot-weakness: #c0392b; | ||
| 2564 | + --swot-opportunity: #1f5ab3; | ||
| 2565 | + --swot-threat: #b36b16; | ||
| 2449 | }} | 2566 | }} |
| 2450 | * {{ box-sizing: border-box; }} | 2567 | * {{ box-sizing: border-box; }} |
| 2451 | body {{ | 2568 | body {{ |
| @@ -2886,6 +3003,150 @@ table th {{ | @@ -2886,6 +3003,150 @@ table th {{ | ||
| 2886 | }} | 3003 | }} |
| 2887 | .align-center {{ text-align: center; }} | 3004 | .align-center {{ text-align: center; }} |
| 2888 | .align-right {{ text-align: right; }} | 3005 | .align-right {{ text-align: right; }} |
| 3006 | +.swot-card {{ | ||
| 3007 | + margin: 26px 0; | ||
| 3008 | + padding: 18px 18px 14px; | ||
| 3009 | + border-radius: 16px; | ||
| 3010 | + border: 1px solid var(--border-color); | ||
| 3011 | + background: linear-gradient(135deg, rgba(76,132,255,0.06), rgba(28,127,110,0.06)), var(--card-bg); | ||
| 3012 | + box-shadow: 0 12px 30px var(--shadow-color); | ||
| 3013 | +}} | ||
| 3014 | +.swot-card__head {{ | ||
| 3015 | + display: flex; | ||
| 3016 | + justify-content: space-between; | ||
| 3017 | + gap: 16px; | ||
| 3018 | + align-items: flex-start; | ||
| 3019 | + flex-wrap: wrap; | ||
| 3020 | +}} | ||
| 3021 | +.swot-card__title {{ | ||
| 3022 | + font-size: 1.15rem; | ||
| 3023 | + font-weight: 750; | ||
| 3024 | + margin-bottom: 4px; | ||
| 3025 | +}} | ||
| 3026 | +.swot-card__summary {{ | ||
| 3027 | + margin: 0; | ||
| 3028 | + color: var(--text-color); | ||
| 3029 | + opacity: 0.85; | ||
| 3030 | +}} | ||
| 3031 | +.swot-legend {{ | ||
| 3032 | + display: flex; | ||
| 3033 | + gap: 8px; | ||
| 3034 | + flex-wrap: wrap; | ||
| 3035 | + align-items: center; | ||
| 3036 | +}} | ||
| 3037 | +.swot-legend__item {{ | ||
| 3038 | + padding: 6px 12px; | ||
| 3039 | + border-radius: 999px; | ||
| 3040 | + font-weight: 700; | ||
| 3041 | + color: #fff; | ||
| 3042 | + box-shadow: 0 4px 10px rgba(0,0,0,0.12); | ||
| 3043 | +}} | ||
| 3044 | +.swot-legend__item.strength {{ background: var(--swot-strength); }} | ||
| 3045 | +.swot-legend__item.weakness {{ background: var(--swot-weakness); }} | ||
| 3046 | +.swot-legend__item.opportunity {{ background: var(--swot-opportunity); }} | ||
| 3047 | +.swot-legend__item.threat {{ background: var(--swot-threat); }} | ||
| 3048 | +.swot-grid {{ | ||
| 3049 | + display: grid; | ||
| 3050 | + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); | ||
| 3051 | + gap: 12px; | ||
| 3052 | + margin-top: 14px; | ||
| 3053 | +}} | ||
| 3054 | +.swot-cell {{ | ||
| 3055 | + border-radius: 14px; | ||
| 3056 | + border: 1px solid rgba(0,0,0,0.05); | ||
| 3057 | + padding: 12px 12px 10px; | ||
| 3058 | + background: linear-gradient(135deg, rgba(255,255,255,0.8), rgba(255,255,255,0.4)); | ||
| 3059 | + box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); | ||
| 3060 | +}} | ||
| 3061 | +.swot-cell.strength {{ border-color: rgba(28,127,110,0.35); background: linear-gradient(135deg, rgba(28,127,110,0.08), rgba(255,255,255,0.75)), var(--card-bg); }} | ||
| 3062 | +.swot-cell.weakness {{ border-color: rgba(192,57,43,0.35); background: linear-gradient(135deg, rgba(192,57,43,0.08), rgba(255,255,255,0.75)), var(--card-bg); }} | ||
| 3063 | +.swot-cell.opportunity {{ border-color: rgba(31,90,179,0.35); background: linear-gradient(135deg, rgba(31,90,179,0.08), rgba(255,255,255,0.75)), var(--card-bg); }} | ||
| 3064 | +.swot-cell.threat {{ border-color: rgba(179,107,22,0.35); background: linear-gradient(135deg, rgba(179,107,22,0.08), rgba(255,255,255,0.75)), var(--card-bg); }} | ||
| 3065 | +.swot-cell__meta {{ | ||
| 3066 | + display: flex; | ||
| 3067 | + gap: 10px; | ||
| 3068 | + align-items: flex-start; | ||
| 3069 | + margin-bottom: 8px; | ||
| 3070 | +}} | ||
| 3071 | +.swot-pill {{ | ||
| 3072 | + display: inline-flex; | ||
| 3073 | + align-items: center; | ||
| 3074 | + justify-content: center; | ||
| 3075 | + width: 36px; | ||
| 3076 | + height: 36px; | ||
| 3077 | + border-radius: 12px; | ||
| 3078 | + font-weight: 800; | ||
| 3079 | + color: #fff; | ||
| 3080 | + box-shadow: 0 6px 16px rgba(0,0,0,0.14); | ||
| 3081 | +}} | ||
| 3082 | +.swot-pill.strength {{ background: var(--swot-strength); }} | ||
| 3083 | +.swot-pill.weakness {{ background: var(--swot-weakness); }} | ||
| 3084 | +.swot-pill.opportunity {{ background: var(--swot-opportunity); }} | ||
| 3085 | +.swot-pill.threat {{ background: var(--swot-threat); }} | ||
| 3086 | +.swot-cell__title {{ | ||
| 3087 | + font-weight: 750; | ||
| 3088 | + letter-spacing: 0.01em; | ||
| 3089 | +}} | ||
| 3090 | +.swot-cell__caption {{ | ||
| 3091 | + font-size: 0.9rem; | ||
| 3092 | + color: var(--text-color); | ||
| 3093 | + opacity: 0.7; | ||
| 3094 | +}} | ||
| 3095 | +.swot-list {{ | ||
| 3096 | + list-style: none; | ||
| 3097 | + padding: 0; | ||
| 3098 | + margin: 0; | ||
| 3099 | + display: flex; | ||
| 3100 | + flex-direction: column; | ||
| 3101 | + gap: 8px; | ||
| 3102 | +}} | ||
| 3103 | +.swot-item {{ | ||
| 3104 | + padding: 10px 12px; | ||
| 3105 | + border-radius: 12px; | ||
| 3106 | + background: rgba(0,0,0,0.02); | ||
| 3107 | + border: 1px dashed rgba(0,0,0,0.05); | ||
| 3108 | +}} | ||
| 3109 | +.swot-item-title {{ | ||
| 3110 | + display: flex; | ||
| 3111 | + justify-content: space-between; | ||
| 3112 | + gap: 8px; | ||
| 3113 | + font-weight: 650; | ||
| 3114 | +}} | ||
| 3115 | +.swot-item-tags {{ | ||
| 3116 | + display: inline-flex; | ||
| 3117 | + gap: 6px; | ||
| 3118 | + flex-wrap: wrap; | ||
| 3119 | + font-size: 0.85rem; | ||
| 3120 | +}} | ||
| 3121 | +.swot-tag {{ | ||
| 3122 | + display: inline-block; | ||
| 3123 | + padding: 4px 8px; | ||
| 3124 | + border-radius: 10px; | ||
| 3125 | + background: rgba(0,0,0,0.06); | ||
| 3126 | + color: var(--text-color); | ||
| 3127 | + line-height: 1.2; | ||
| 3128 | +}} | ||
| 3129 | +.swot-tag.neutral {{ | ||
| 3130 | + background: rgba(0,0,0,0.04); | ||
| 3131 | +}} | ||
| 3132 | +.swot-item-desc {{ | ||
| 3133 | + margin-top: 4px; | ||
| 3134 | + color: var(--text-color); | ||
| 3135 | + opacity: 0.92; | ||
| 3136 | +}} | ||
| 3137 | +.swot-item-evidence {{ | ||
| 3138 | + margin-top: 4px; | ||
| 3139 | + font-size: 0.9rem; | ||
| 3140 | + color: var(--secondary-color); | ||
| 3141 | +}} | ||
| 3142 | +.swot-empty {{ | ||
| 3143 | + padding: 12px; | ||
| 3144 | + border-radius: 12px; | ||
| 3145 | + border: 1px dashed var(--border-color); | ||
| 3146 | + text-align: center; | ||
| 3147 | + color: var(--text-color); | ||
| 3148 | + opacity: 0.7; | ||
| 3149 | +}} | ||
| 2889 | .callout {{ | 3150 | .callout {{ |
| 2890 | border-left: 4px solid var(--primary-color); | 3151 | border-left: 4px solid var(--primary-color); |
| 2891 | padding: 16px; | 3152 | padding: 16px; |
| @@ -3108,6 +3369,7 @@ pre.code-block {{ | @@ -3108,6 +3369,7 @@ pre.code-block {{ | ||
| 3108 | .engine-quote, | 3369 | .engine-quote, |
| 3109 | .chart-card, | 3370 | .chart-card, |
| 3110 | .kpi-grid, | 3371 | .kpi-grid, |
| 3372 | + .swot-card, | ||
| 3111 | .table-wrap, | 3373 | .table-wrap, |
| 3112 | figure, | 3374 | figure, |
| 3113 | blockquote {{ | 3375 | blockquote {{ |
| @@ -3133,6 +3395,11 @@ pre.code-block {{ | @@ -3133,6 +3395,11 @@ pre.code-block {{ | ||
| 3133 | height: auto !important; | 3395 | height: auto !important; |
| 3134 | max-width: 100% !important; | 3396 | max-width: 100% !important; |
| 3135 | }} | 3397 | }} |
| 3398 | + .swot-card, | ||
| 3399 | + .swot-cell {{ | ||
| 3400 | + break-inside: avoid; | ||
| 3401 | + page-break-inside: avoid; | ||
| 3402 | + }} | ||
| 3136 | .table-wrap {{ | 3403 | .table-wrap {{ |
| 3137 | overflow-x: auto; | 3404 | overflow-x: auto; |
| 3138 | max-width: 100%; | 3405 | max-width: 100%; |
-
Please register or login to post a comment