Terminal UI stil, markdown tabele i prikaz troškova
All checks were successful
Tests / unit-tests (push) Successful in 8s
All checks were successful
Tests / unit-tests (push) Successful in 8s
- Dodat goldmark sa Table/Strikethrough/TaskList ekstenzijama (markdown.go) - Prepisana CSS tema na konzolni stil (JetBrains Mono, tamna pozadina, prompt prefix) - Prikaz troškova i trajanja posle svakog Claude odgovora (duration, cost, turns) - Ispravljen parsing result eventa (json.RawMessage + top-level polja) - Ispravljen concurrent write bug na WebSocket (write channel pattern) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9d0e507689
commit
0ce03c27e3
@ -29,8 +29,10 @@ type CLIEvent struct {
|
|||||||
RawResult json.RawMessage `json:"result,omitempty"`
|
RawResult json.RawMessage `json:"result,omitempty"`
|
||||||
Result *CLIResult `json:"-"`
|
Result *CLIResult `json:"-"`
|
||||||
|
|
||||||
// Top-level cost field (present when result is string)
|
// Top-level fields on result events
|
||||||
TotalCostUSD float64 `json:"total_cost_usd,omitempty"`
|
TotalCostUSD float64 `json:"total_cost_usd,omitempty"`
|
||||||
|
DurationMS float64 `json:"duration_ms,omitempty"`
|
||||||
|
NumTurns int `json:"num_turns,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamEvent is the inner event inside a stream_event wrapper.
|
// StreamEvent is the inner event inside a stream_event wrapper.
|
||||||
@ -213,14 +215,25 @@ func (cp *CLIProcess) readOutput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse result field — can be string or object
|
// Parse result field — can be string or object
|
||||||
if len(event.RawResult) > 0 {
|
if event.Type == "result" {
|
||||||
var result CLIResult
|
if len(event.RawResult) > 0 {
|
||||||
if err := json.Unmarshal(event.RawResult, &result); err == nil {
|
var result CLIResult
|
||||||
event.Result = &result
|
if err := json.Unmarshal(event.RawResult, &result); err == nil {
|
||||||
|
event.Result = &result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If it's a string, Result stays nil — cost comes from top-level field
|
// Always build Result from top-level fields (they're always present on result events)
|
||||||
if event.Result == nil && event.TotalCostUSD > 0 {
|
if event.Result == nil {
|
||||||
event.Result = &CLIResult{CostUSD: event.TotalCostUSD}
|
event.Result = &CLIResult{}
|
||||||
|
}
|
||||||
|
if event.TotalCostUSD > 0 {
|
||||||
|
event.Result.CostUSD = event.TotalCostUSD
|
||||||
|
}
|
||||||
|
if event.DurationMS > 0 {
|
||||||
|
event.Result.Duration = event.DurationMS
|
||||||
|
}
|
||||||
|
if event.NumTurns > 0 {
|
||||||
|
event.Result.NumTurns = event.NumTurns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
files.go
10
files.go
@ -1,14 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yuin/goldmark"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
@ -102,10 +99,5 @@ func RenderMarkdownFile(projectDir, relPath string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
return RenderMD(content), nil
|
||||||
if err := goldmark.Convert([]byte(content), &buf); err != nil {
|
|
||||||
return "", fmt.Errorf("render markdown: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
29
markdown.go
Normal file
29
markdown.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
)
|
||||||
|
|
||||||
|
var md goldmark.Markdown
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
md = goldmark.New(
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
extension.Table,
|
||||||
|
extension.Strikethrough,
|
||||||
|
extension.TaskList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderMD converts markdown text to HTML with table support.
|
||||||
|
func RenderMD(text string) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := md.Convert([]byte(text), &buf); err != nil {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
261
static/style.css
261
static/style.css
@ -178,14 +178,14 @@ a:hover {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat layout */
|
/* Chat layout — terminal style */
|
||||||
.chat-container {
|
.chat-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 300px;
|
width: 280px;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -194,7 +194,7 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
padding: 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -202,7 +202,7 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header h3 {
|
.sidebar-header h3 {
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,10 +213,11 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-item {
|
.file-item {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.4rem 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -228,33 +229,33 @@ a:hover {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item.active {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-main {
|
.chat-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
background: #0d1117;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.5rem 1rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-header h2 {
|
.chat-header h2 {
|
||||||
font-size: 1.1rem;
|
font-size: 0.9rem;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-header .status {
|
.chat-header .status {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,69 +263,82 @@ a:hover {
|
|||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Terminal output area */
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1rem 1.5rem;
|
padding: 0.75rem 1rem;
|
||||||
display: flex;
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
flex-direction: column;
|
font-size: 0.85rem;
|
||||||
gap: 1rem;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* User input — prompt style */
|
||||||
.message {
|
.message {
|
||||||
max-width: 85%;
|
padding: 0.3rem 0;
|
||||||
padding: 0.8rem 1.2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-user {
|
.message-user {
|
||||||
align-self: flex-end;
|
color: var(--success);
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-assistant {
|
|
||||||
align-self: flex-start;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-assistant .content {
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-system {
|
.message-user::before {
|
||||||
align-self: center;
|
content: "❯ ";
|
||||||
background: transparent;
|
color: var(--accent);
|
||||||
color: var(--text-muted);
|
font-weight: bold;
|
||||||
font-size: 0.85rem;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Claude response */
|
||||||
|
.message-assistant {
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
border-left: 2px solid var(--border);
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
margin: 0.3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant .content {
|
||||||
|
/* rendered markdown, no pre-wrap */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System messages */
|
||||||
|
.message-system {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tool calls — command style */
|
||||||
.message-tool {
|
.message-tool {
|
||||||
align-self: flex-start;
|
|
||||||
background: rgba(15, 52, 96, 0.5);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
max-width: 90%;
|
font-size: 0.8rem;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-tool .tool-name {
|
.message-tool .tool-name {
|
||||||
color: var(--warning);
|
color: var(--warning);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.3rem;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-tool .tool-name::before {
|
||||||
|
content: "⚙ ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-tool div {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typing indicator */
|
||||||
.typing-indicator {
|
.typing-indicator {
|
||||||
align-self: flex-start;
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-size: 0.8rem;
|
||||||
font-size: 0.85rem;
|
padding: 0.2rem 0;
|
||||||
padding: 0.5rem 0;
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typing-indicator .dots {
|
.typing-indicator .dots {
|
||||||
@ -338,29 +352,31 @@ a:hover {
|
|||||||
80%, 100% { opacity: 1; }
|
80%, 100% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Input area — terminal prompt */
|
||||||
.chat-input-area {
|
.chat-input-area {
|
||||||
padding: 1rem 1.5rem;
|
padding: 0.5rem 1rem;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
background: var(--bg-secondary);
|
background: #0d1117;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-form {
|
.chat-input-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.5rem 0.75rem;
|
||||||
background: var(--bg-input);
|
background: transparent;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 0.85rem;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
font-family: inherit;
|
min-height: 36px;
|
||||||
min-height: 44px;
|
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,28 +385,87 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-form .btn {
|
.chat-input-form .btn {
|
||||||
align-self: flex-end;
|
padding: 0.5rem 1rem;
|
||||||
}
|
font-size: 0.8rem;
|
||||||
|
|
||||||
/* Code blocks in messages */
|
|
||||||
.message pre {
|
|
||||||
background: var(--code-bg);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.8rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message code {
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message p code {
|
/* Markdown rendered content */
|
||||||
background: var(--code-bg);
|
.message-assistant pre {
|
||||||
padding: 0.15rem 0.4rem;
|
background: #161b22;
|
||||||
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
padding: 0.6rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.4rem 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant code {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant p code {
|
||||||
|
background: #161b22;
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant h1,
|
||||||
|
.message-assistant h2,
|
||||||
|
.message-assistant h3 {
|
||||||
|
color: var(--accent);
|
||||||
|
margin: 0.6rem 0 0.3rem;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant h1 { font-size: 1.1em; }
|
||||||
|
.message-assistant h2 { font-size: 1em; }
|
||||||
|
|
||||||
|
.message-assistant ul,
|
||||||
|
.message-assistant ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant p {
|
||||||
|
margin: 0.3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.message-assistant table,
|
||||||
|
.file-viewer-content table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant th,
|
||||||
|
.message-assistant td,
|
||||||
|
.file-viewer-content th,
|
||||||
|
.file-viewer-content td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant th,
|
||||||
|
.file-viewer-content th {
|
||||||
|
background: #161b22;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant tr:nth-child(even),
|
||||||
|
.file-viewer-content tr:nth-child(even) {
|
||||||
|
background: rgba(22, 27, 34, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* File viewer overlay */
|
/* File viewer overlay */
|
||||||
@ -400,7 +475,7 @@ a:hover {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: var(--bg-secondary);
|
background: #0d1117;
|
||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--border);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -409,31 +484,35 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-header {
|
.file-viewer-header {
|
||||||
padding: 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-content {
|
.file-viewer-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-content h1,
|
.file-viewer-content h1,
|
||||||
.file-viewer-content h2,
|
.file-viewer-content h2,
|
||||||
.file-viewer-content h3 {
|
.file-viewer-content h3 {
|
||||||
margin: 1rem 0 0.5rem;
|
margin: 0.8rem 0 0.4rem;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-content pre {
|
.file-viewer-content pre {
|
||||||
background: var(--code-bg);
|
background: #161b22;
|
||||||
border-radius: 6px;
|
border: 1px solid var(--border);
|
||||||
padding: 1rem;
|
border-radius: 4px;
|
||||||
|
padding: 0.8rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,18 +520,22 @@ a:hover {
|
|||||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-viewer-content p {
|
||||||
|
margin: 0.4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Scrollbar */
|
/* Scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: var(--bg-primary);
|
background: #0d1117;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
|||||||
39
ws.go
39
ws.go
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -11,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/yuin/goldmark"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
@ -228,13 +226,30 @@ func (wh *WSHandler) listenEvents(sess *ChatSession) {
|
|||||||
Content: fragment,
|
Content: fragment,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
})
|
})
|
||||||
if event.Result != nil && event.Result.CostUSD > 0 {
|
if event.Result != nil {
|
||||||
costMsg := FragmentSystemMessage(fmt.Sprintf("Gotovo (%.4f USD)", event.Result.CostUSD))
|
parts := []string{}
|
||||||
sess.AddMessage(ChatMessage{
|
if event.Result.Duration > 0 {
|
||||||
Role: "system",
|
secs := event.Result.Duration / 1000
|
||||||
Content: costMsg,
|
if secs >= 60 {
|
||||||
Timestamp: time.Now(),
|
parts = append(parts, fmt.Sprintf("%.0fm %.0fs", secs/60, float64(int(secs)%60)))
|
||||||
})
|
} else {
|
||||||
|
parts = append(parts, fmt.Sprintf("%.1fs", secs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if event.Result.CostUSD > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("$%.4f", event.Result.CostUSD))
|
||||||
|
}
|
||||||
|
if event.Result.NumTurns > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d turn(s)", event.Result.NumTurns))
|
||||||
|
}
|
||||||
|
if len(parts) > 0 {
|
||||||
|
costMsg := FragmentSystemMessage(strings.Join(parts, " · "))
|
||||||
|
sess.AddMessage(ChatMessage{
|
||||||
|
Role: "system",
|
||||||
|
Content: costMsg,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,9 +289,5 @@ func (wh *WSHandler) handleStreamEvent(sess *ChatSession, se *StreamEvent, curre
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderMarkdown(text string) string {
|
func renderMarkdown(text string) string {
|
||||||
var buf bytes.Buffer
|
return RenderMD(text)
|
||||||
if err := goldmark.Convert([]byte(text), &buf); err != nil {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user