No messages in this session.
"); @@ -370,7 +473,12 @@ pub fn render_session_html( } out.push_str("diff --git a/Cargo.lock b/Cargo.lock index 7e96e5d..8716d64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1422,6 +1422,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.0", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -2554,6 +2563,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags 2.13.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "pulp" version = "0.21.5" @@ -2734,6 +2762,7 @@ dependencies = [ "fs2", "globset", "hf-hub", + "pulldown-cmark", "ratatui", "rusqlite", "serde", @@ -3691,6 +3720,12 @@ dependencies = [ "ug", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index f6ac8c6..d9b04a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ ratatui = "0.29" crossterm = "0.28" unicode-width = "0.2" fs2 = "0.4" +pulldown-cmark = "0.13" [target.'cfg(target_os = "macos")'.dependencies] candle-core = { version = "0.10", features = ["metal"] } diff --git a/src/share.rs b/src/share.rs index 6a54a61..71610f3 100644 --- a/src/share.rs +++ b/src/share.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{Context, Result, anyhow, bail}; +use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd, html}; use serde_json::Value; use crate::config::{AppConfig, ShareConfig}; @@ -14,7 +15,7 @@ const PROVIDER_CLOUDFLARE_PAGES: &str = "cloudflare-pages"; const PAGES_PROJECT_NAME_FIELD: &str = "Project Name"; const PAGES_PROJECT_DOMAINS_FIELD: &str = "Project Domains"; const MAX_PAGES_ASSET_BYTES: usize = 25 * 1024 * 1024; -const HEADERS: &str = "/*\n X-Robots-Tag: noindex, nofollow\n X-Frame-Options: DENY\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n"; +const HEADERS: &str = "/*\n X-Robots-Tag: noindex, nofollow\n X-Frame-Options: DENY\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n Cache-Control: no-store\n"; const ROBOTS: &str = "User-agent: *\nDisallow: /\n"; #[derive(Debug, Clone)] @@ -30,81 +31,179 @@ pub struct SharePreview { } const SESSION_PAGE_CSS: &str = r#" -:root { - --page-bg: #F5F5F7; - --content-bg: #FFFFFF; - --text-primary: #1D1D1F; - --text-secondary: #86868B; - --user-block-bg: #FFF3D6; - --user-block-border: rgba(255, 149, 0, 0.22); - --user-block-accent: #FF9500; - --log-border: #E5E5EA; - --layout-width: 1040px; - --code-bg: #1E1E1E; - --code-text: #F5F5F7; - --border-radius: 12px; - --read-width: 700px; - --font-system: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; - --font-mono: "SF Mono", Menlo, monospace; +:root{ + --page-bg:#FAF9F6;--surface:#FFFFFF;--user-surface:#FFFFFF; + --text-primary:#23211C;--text-secondary:#6C685F;--text-tertiary:#9A958A; + --accent:#3C4FA0;--accent-soft:rgba(60,79,160,.09); + --rule:rgba(35,33,28,.10);--rule-strong:rgba(35,33,28,.16); + --code-bg:#F4F2EC;--tool-bg:rgba(35,33,28,.028); + --read-width:716px;--layout-width:1100px; + --font-serif:"Newsreader",Georgia,"Times New Roman","Songti SC","STSong","Source Han Serif SC","Noto Serif CJK SC",SimSun,serif; + --font-sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",sans-serif; + --font-mono:"JetBrains Mono","SF Mono",Menlo,"PingFang SC","Microsoft YaHei",monospace; } *,*::before,*::after{box-sizing:border-box} -body{margin:0;overflow-x:hidden;background:var(--page-bg);color:var(--text-primary);font:15px/1.65 var(--font-system);-webkit-font-smoothing:antialiased} -.site-header{position:sticky;top:0;z-index:10;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);background:rgba(255,255,255,.72);border-bottom:1px solid rgba(0,0,0,.05)} -.site-header-inner{max-width:var(--layout-width);margin:0 auto;padding:22px 24px 18px} -.site-header h1{margin:0 0 6px;font-size:22px;font-weight:600;line-height:1.3;letter-spacing:-.02em;color:var(--text-primary)} -.meta{margin:0;color:var(--text-secondary);font-size:13px;line-height:1.5} -.layout{display:flex;align-items:flex-start;justify-content:center;gap:36px;max-width:var(--layout-width);margin:0 auto;padding:28px 24px 64px} -.page{flex:0 1 var(--read-width);min-width:0;padding:0;margin:0} -.document{min-width:0;background:var(--content-bg);border-radius:var(--border-radius);padding:40px 36px 52px} -.user-toc{flex:0 0 220px;position:sticky;top:88px;max-height:calc(100vh - 104px);overflow-y:auto;padding:4px 0 12px} -.user-toc-title{margin:0 0 12px;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text-secondary)} -.user-toc nav{display:flex;flex-direction:column;gap:2px} -.user-toc a{display:block;padding:7px 10px;border-left:2px solid transparent;border-radius:0 8px 8px 0;color:var(--text-secondary);font-size:12px;line-height:1.45;text-decoration:none;transition:color .15s ease,background .15s ease,border-color .15s ease} -.user-toc a:hover{color:var(--text-primary);background:rgba(0,0,0,.03);border-left-color:var(--user-block-accent)} +html{-webkit-text-size-adjust:100%;scroll-behavior:smooth} +body{margin:0;background:var(--page-bg);color:var(--text-primary);font:16px/1.6 var(--font-sans);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility} + +.site-header{position:sticky;top:0;z-index:10;backdrop-filter:saturate(140%) blur(16px);-webkit-backdrop-filter:saturate(140%) blur(16px);background:rgba(250,249,246,.82);border-bottom:1px solid var(--rule)} +.site-header-inner{max-width:var(--layout-width);margin:0 auto;padding:18px 32px 16px} +.site-header h1{margin:0;max-width:var(--read-width);font:600 29px/1.22 var(--font-serif);letter-spacing:-.01em;color:var(--text-primary)} +.meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;margin:13px 0 2px;color:var(--text-secondary);font-size:13px} +.meta-item{display:inline-flex;align-items:center;gap:6px;white-space:nowrap} +.meta-sep{width:3px;height:3px;border-radius:50%;background:var(--text-tertiary);opacity:.7} +.meta-tags{display:inline-flex;flex-wrap:wrap;gap:6px} +.meta-tag{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border:1px solid var(--rule);border-radius:999px;background:var(--surface);font:500 11.5px/1.5 var(--font-mono);color:var(--text-secondary)} +.meta-tag b{font-weight:500;color:var(--text-primary)} + +.layout{display:grid;grid-template-columns:minmax(0,var(--read-width)) 212px;grid-template-areas:"page toc";justify-content:center;gap:64px;max-width:var(--layout-width);margin:0 auto;padding:40px 32px 28px} +.page{grid-area:page;min-width:0} +.document{min-width:0} + +.user-toc{grid-area:toc;position:sticky;top:50vh;transform:translateY(-50%);align-self:start;max-height:80vh;display:flex;flex-direction:column;align-items:flex-end;gap:4px;padding:4px 0;z-index:5} +.user-toc-title{margin:0 3px 4px 0;font:600 10px/1 var(--font-sans);letter-spacing:.12em;text-transform:uppercase;color:var(--text-tertiary);opacity:0;transform:translateX(4px);transition:opacity .16s ease,transform .16s ease} +.user-toc:hover .user-toc-title{opacity:1;transform:none} +.toc-nav-btn{display:flex;align-items:center;justify-content:center;width:26px;height:20px;margin-right:-3px;border:0;background:none;color:var(--text-tertiary);cursor:pointer;border-radius:6px;transition:color .15s,background .15s} +.toc-nav-btn:hover{color:var(--accent);background:var(--accent-soft)} +.toc-nav-btn svg{width:13px;height:13px} +.toc-ticks{display:flex;flex-direction:column;gap:3px;width:100%;align-items:flex-end} +.tick{display:flex;align-items:center;justify-content:flex-end;gap:9px;height:22px;padding:0 3px 0 8px;text-decoration:none;border-radius:7px;color:var(--text-secondary);transition:background .14s} +.tick-label{display:flex;align-items:baseline;justify-content:flex-end;gap:9px;max-width:0;opacity:0;overflow:hidden;white-space:nowrap;transition:max-width .22s ease,opacity .16s ease} +.user-toc:hover .tick-label{max-width:165px;opacity:1} +.tick-n{flex:none;font:500 10.5px/1.5 var(--font-mono);color:var(--text-tertiary)} +.tick-t{font-size:12px;line-height:1.35;color:inherit;overflow:hidden;text-overflow:ellipsis} +.tick-line{flex:none;width:18px;height:2px;border-radius:2px;background:var(--rule-strong);transition:width .2s ease,background .2s ease} +.tick.active .tick-line{width:30px;background:var(--accent)} +.user-toc:hover .tick.active .tick-line{width:24px} +.tick:hover{background:var(--accent-soft)} +.tick:hover .tick-t,.tick:hover .tick-n{color:var(--text-primary)} +.tick.active .tick-t{color:var(--text-primary);font-weight:500} +.tick.active .tick-n{color:var(--accent)} + .turn{margin:0} -.turn.user{scroll-margin-top:96px} -.role-label{display:block;margin:0 0 10px;font-size:12px;font-weight:500;letter-spacing:.02em;color:var(--text-secondary)} -.turn.user .role-label{color:#B25000} -.turn.assistant .role-label{color:#5856D6} -.turn.user:not(:first-child){margin-top:48px;padding-top:36px;border-top:1px solid var(--log-border)} -.turn.assistant{margin-top:24px} -.turn.user+.turn.assistant{margin-top:20px} -.user-block{min-width:0;max-width:100%;background:var(--user-block-bg);border-radius:var(--border-radius);padding:16px 20px;border:1px solid var(--user-block-border);box-shadow:inset 3px 0 0 var(--user-block-accent)} -.assistant-body{min-width:0;max-width:100%;color:var(--text-primary);font-size:16px} +.turn.user{scroll-margin-top:128px} +.turn.user+.turn.assistant,.turn.assistant{margin-top:20px} +.turn.user:not(:first-child){margin-top:44px} +.role-label{display:inline-flex;align-items:center;gap:7px;margin:0 0 11px;font:600 11px/1 var(--font-sans);letter-spacing:.09em;text-transform:uppercase} +.role-label::before{content:"";width:14px;height:1px;background:currentColor;opacity:.5} +.turn.user .role-label{color:var(--text-tertiary)} +.turn.assistant .role-label{color:var(--accent)} + +.user-block{min-width:0;background:var(--user-surface);border:1px solid var(--rule);border-left:3px solid var(--rule-strong);border-radius:4px 10px 10px 4px;padding:15px 18px;box-shadow:0 1px 2px rgba(35,33,28,.04)} +.user-block .prose{font-family:var(--font-sans);font-size:15.5px;line-height:1.62;color:var(--text-primary)} + +.assistant-body{min-width:0;max-width:100%;color:var(--text-primary)} +.assistant-body>.prose+.prose{margin-top:.7em} +.assistant-body .prose{font:18px/1.72 var(--font-serif)} +.assistant-body .tool-run{margin:1.25em 0} .prose{min-width:0;max-width:100%;overflow-wrap:anywhere;word-break:break-word} -.assistant-body .tool-run{margin:1.4em 0} -.tool-group{margin:0;color:var(--text-secondary);font-size:13px;line-height:1.5} -.tool-group>summary{cursor:pointer;list-style:none;color:var(--text-secondary);padding:2px 0} -.tool-group>summary::-webkit-details-marker{display:none} -.tool-group>summary::before{content:"▸ ";display:inline-block;transition:transform .15s ease} -.tool-group[open]>summary::before{transform:rotate(90deg)} -.tool-group-items{margin:8px 0 0;padding:0 0 0 8px} -.prose p{margin:0 0 1.2em} +.prose p{margin:0 0 .8em;text-wrap:pretty} .prose p:last-child{margin-bottom:0} -.prose h2,.prose h3{margin:1.4em 0 .6em;font-weight:600;line-height:1.35;letter-spacing:-.02em;color:var(--text-primary)} -.prose h2{font-size:19px} -.prose h3{font-size:17px} -.prose ul{margin:0 0 1.2em;padding-left:1.4em} -.prose li{margin:0 0 .45em} +.prose h1,.prose h2,.prose h3,.prose h4{font-family:var(--font-serif);font-weight:600;line-height:1.3;letter-spacing:-.01em;color:var(--text-primary)} +.prose h1{margin:1.5em 0 .5em;font-size:25px} +.prose h2{margin:1.5em 0 .5em;font-size:23px} +.prose h3{margin:1.3em 0 .4em;font-size:19px} +.prose h4{margin:1.3em 0 .4em;font-size:16px} +.prose ul,.prose ol{margin:0 0 .9em;padding-left:1.25em} +.prose li{margin:0 0 .4em} +.prose li>p{margin:.35em 0} +.prose li::marker{color:var(--text-tertiary)} .prose strong{font-weight:600} -.prose code{font:13px/1.5 var(--font-mono);background:var(--user-block-bg);border-radius:4px;padding:2px 6px;color:var(--text-primary)} -pre.code-block,pre.preformatted{margin:1.2em 0;padding:16px;border-radius:8px;font:13px/1.55 var(--font-mono);overflow-x:auto;white-space:pre;word-break:normal;max-width:100%} -pre.code-block{background:var(--code-bg);color:var(--code-text)} -pre.preformatted{background:rgba(0,0,0,.04);color:var(--text-primary)} -.tool-run{margin:1.2em 0;padding:10px 0 10px 14px;border-left:3px solid var(--log-border)} +.prose em{font-style:italic} +.prose del{color:var(--text-secondary)} +.prose a{color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent-soft)} +.prose a:hover{border-bottom-color:var(--accent)} +.prose blockquote{margin:1em 0;padding:.2em 0 .2em 14px;border-left:3px solid var(--rule-strong);color:var(--text-secondary)} +.prose blockquote p{margin:.45em 0} +.prose hr{border:0;border-top:1px solid var(--rule);margin:1.4em 0} +.prose table{width:100%;border-collapse:collapse;margin:1em 0;font-size:14px;line-height:1.45;display:block;overflow-x:auto} +.prose th,.prose td{border:1px solid var(--rule);padding:7px 9px;text-align:left;vertical-align:top} +.prose th{background:var(--code-bg);font-weight:600} +.prose input[type="checkbox"]{width:14px;height:14px;margin:0 .45em 0 0;vertical-align:-2px} +.prose code{font:.86em/1.5 var(--font-mono);background:var(--accent-soft);border-radius:5px;padding:1.5px 5px;color:#39406b} + +pre.code-block,pre.preformatted,.prose pre{margin:1.1em 0;padding:15px 17px;border:1px solid var(--rule);border-radius:10px;font:13px/1.62 var(--font-mono);overflow-x:auto;white-space:pre;word-break:normal;max-width:100%;color:var(--text-primary)} +pre.code-block{background:var(--code-bg)} +pre.code-block code,.prose pre code{background:transparent;border-radius:0;padding:0;color:inherit;font:inherit} +pre.preformatted{background:var(--surface);color:var(--text-secondary)} + +.tool-run{margin:1.25em 0;padding:6px 4px 6px 16px;border-left:2px solid var(--rule)} .tool-run .tool-group{border-left:0;padding:0;margin:0} -.tool-run .log,.tool-group-items .log{margin:0 0 6px;padding:0 0 0 10px;border-left:2px solid var(--log-border)} -.tool-run .log:last-child,.tool-group-items .log:last-child{margin-bottom:0} -.empty{margin:0;color:var(--text-secondary);font-size:15px;line-height:1.65} -@media (max-width:960px){.layout{display:block;padding:28px 20px 64px}.user-toc{display:none}} -.log{margin:1em 0;padding:0 0 0 14px;border-left:3px solid var(--log-border);color:var(--text-secondary);font-size:13px;line-height:1.5} -.log summary{cursor:pointer;list-style:none;color:var(--text-secondary)} +.tool-group{margin:0;color:var(--text-secondary);font-size:13px;line-height:1.5} +.tool-group>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px;padding:3px 0;font:500 12.5px/1.4 var(--font-sans);color:var(--text-secondary)} +.tool-group>summary::-webkit-details-marker{display:none} +.tool-group>summary .chev,.log summary .chev{flex:none;width:14px;height:14px;color:var(--text-tertiary);transition:transform .15s} +.tool-group[open]>summary .chev,.log[open] summary .chev{transform:rotate(90deg)} +.tool-group>summary .count{margin-left:2px;padding:1px 7px;border-radius:999px;background:var(--accent-soft);font:500 11px/1.5 var(--font-mono);color:var(--accent)} +.tool-group-items{margin:7px 0 0;padding:0 0 0 6px;display:flex;flex-direction:column;gap:2px} + +.log{margin:.6em 0;color:var(--text-secondary);font-size:13px;line-height:1.5} +.tool-run .log,.tool-group-items .log{margin:0} +.log summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px;padding:5px 9px;border-radius:7px;font:500 12.5px/1.4 var(--font-sans);color:var(--text-secondary);transition:background .12s} +.log summary:hover{background:var(--tool-bg)} .log summary::-webkit-details-marker{display:none} -.log summary::before{content:"▸ ";display:inline-block;transition:transform .15s ease} -.log[open] summary::before{transform:rotate(90deg)} -.log pre{margin:8px 0 0;padding:0;background:transparent;border:0;color:var(--text-secondary);font:13px/1.5 var(--font-mono);white-space:pre-wrap;word-break:break-word} +.log summary .badge{flex:none;font:500 10px/1.5 var(--font-mono);letter-spacing:.04em;text-transform:uppercase;padding:1px 7px;border-radius:5px;background:var(--tool-bg);color:var(--text-tertiary)} +.log summary .lname{font-family:var(--font-mono);font-size:12px;color:var(--text-primary)} +.log[open] summary{color:var(--text-primary)} +.log pre{margin:6px 0 4px;padding:11px 13px;background:var(--tool-bg);border:1px solid var(--rule);border-radius:8px;color:var(--text-secondary);font:12px/1.55 var(--font-mono);white-space:pre-wrap;word-break:break-word;overflow-x:auto} + +.empty{margin:0;color:var(--text-secondary);font:18px/1.65 var(--font-serif)} + +.site-footer{max-width:var(--layout-width);margin:0 auto;padding:22px 32px 60px} +.site-footer-inner{max-width:var(--read-width);margin-left:auto;margin-right:auto;display:flex;align-items:center;justify-content:center;gap:10px;padding-top:22px;border-top:1px solid var(--rule);color:var(--text-tertiary);font-size:12.5px} +.site-footer .brand-dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)} +.site-footer b{font-weight:600;color:var(--text-secondary)} + +@media (max-width:980px){ + .site-header-inner{padding:16px 20px 14px} + .layout{display:block;padding:28px 20px 24px} + .user-toc{display:none} + .site-footer{padding:18px 20px 56px} + .assistant-body .prose{font-size:17px} +} "#; +const CHEVRON_SVG: &str = ""; + +const TOC_NAV_SCRIPT: &str = r#""#; + pub fn default_project_name() -> String { let suffix = uuid::Uuid::new_v4().simple().to_string(); format!("recall-share-{}", &suffix[..6]) @@ -338,6 +437,10 @@ pub fn render_session_html( out.push_str("
"); out.push_str(""); out.push_str(""); + out.push_str(""); + out.push_str(""); + out.push_str(""); + out.push_str(""); out.push_str("No messages in this session.
"); @@ -370,7 +473,12 @@ pub fn render_session_html( } out.push_str("