All checks were successful
Tests / unit-tests (push) Successful in 41s
- Koristi --input-format stream-json za multi-turn razgovor - Koristi --include-partial-messages za streaming chunk-ove - Filtrira CLAUDECODE i CLAUDE_CODE_ENTRYPOINT env varijable - Svi WS write-ovi idu kroz jedan kanal (nema concurrent write panic) - Tool call prikaz: Read prikazuje putanju, Bash prikazuje komandu, itd - result polje moze biti string ili objekat (oba obradjena) - Subscriber/broadcast model za real-time push Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
144 lines
4.7 KiB
Go
144 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html"
|
|
"strings"
|
|
)
|
|
|
|
// FragmentUserMessage returns an HTML fragment for a user message.
|
|
func FragmentUserMessage(text string) string {
|
|
escaped := html.EscapeString(text)
|
|
return fmt.Sprintf(`<div id="chat-messages" hx-swap-oob="beforeend"><div class="message message-user">%s</div></div>`, escaped)
|
|
}
|
|
|
|
// FragmentAssistantStart returns the opening tag for an assistant message with streaming.
|
|
func FragmentAssistantStart(msgID string) string {
|
|
return fmt.Sprintf(`<div id="chat-messages" hx-swap-oob="beforeend"><div class="message message-assistant" id="%s"><div class="content"></div></div></div>`, msgID)
|
|
}
|
|
|
|
// FragmentAssistantChunk appends text to an existing assistant message.
|
|
func FragmentAssistantChunk(msgID, textChunk string) string {
|
|
escaped := html.EscapeString(textChunk)
|
|
return fmt.Sprintf(`<div id="%s" hx-swap-oob="beforeend:.content">%s</div>`, msgID, escaped)
|
|
}
|
|
|
|
// FragmentAssistantComplete replaces the content of an assistant message with final rendered content.
|
|
func FragmentAssistantComplete(msgID, htmlContent string) string {
|
|
return fmt.Sprintf(`<div id="%s" hx-swap-oob="innerHTML:.content">%s</div>`, msgID, htmlContent)
|
|
}
|
|
|
|
// FragmentToolCall returns an HTML fragment for a tool use notification.
|
|
func FragmentToolCall(toolName string, toolInput string) string {
|
|
escapedName := html.EscapeString(toolName)
|
|
summary := formatToolSummary(toolName, toolInput)
|
|
return fmt.Sprintf(`<div id="chat-messages" hx-swap-oob="beforeend"><div class="message message-tool"><div class="tool-name">%s</div><div>%s</div></div></div>`, escapedName, summary)
|
|
}
|
|
|
|
// formatToolSummary produces a human-readable summary of a tool call.
|
|
func formatToolSummary(toolName, rawInput string) string {
|
|
var inputMap map[string]any
|
|
if err := json.Unmarshal([]byte(rawInput), &inputMap); err != nil {
|
|
// Not JSON, just escape and truncate
|
|
s := html.EscapeString(rawInput)
|
|
if len(s) > 200 {
|
|
s = s[:200] + "..."
|
|
}
|
|
return s
|
|
}
|
|
|
|
switch toolName {
|
|
case "Read":
|
|
if fp, ok := inputMap["file_path"].(string); ok {
|
|
return html.EscapeString(fp)
|
|
}
|
|
case "Edit":
|
|
if fp, ok := inputMap["file_path"].(string); ok {
|
|
return html.EscapeString(fmt.Sprintf("%s", fp))
|
|
}
|
|
case "Write":
|
|
if fp, ok := inputMap["file_path"].(string); ok {
|
|
return html.EscapeString(fp)
|
|
}
|
|
case "Bash":
|
|
if cmd, ok := inputMap["command"].(string); ok {
|
|
s := cmd
|
|
if len(s) > 150 {
|
|
s = s[:150] + "..."
|
|
}
|
|
return "<code>" + html.EscapeString(s) + "</code>"
|
|
}
|
|
case "Glob":
|
|
if pat, ok := inputMap["pattern"].(string); ok {
|
|
return html.EscapeString(pat)
|
|
}
|
|
case "Grep":
|
|
parts := []string{}
|
|
if pat, ok := inputMap["pattern"].(string); ok {
|
|
parts = append(parts, pat)
|
|
}
|
|
if p, ok := inputMap["path"].(string); ok {
|
|
parts = append(parts, "in "+p)
|
|
}
|
|
if len(parts) > 0 {
|
|
return html.EscapeString(strings.Join(parts, " "))
|
|
}
|
|
case "WebSearch":
|
|
if q, ok := inputMap["query"].(string); ok {
|
|
return html.EscapeString(q)
|
|
}
|
|
case "WebFetch":
|
|
if u, ok := inputMap["url"].(string); ok {
|
|
return html.EscapeString(u)
|
|
}
|
|
}
|
|
|
|
// Fallback: show key=value pairs
|
|
var parts []string
|
|
for k, v := range inputMap {
|
|
s := fmt.Sprintf("%v", v)
|
|
if len(s) > 80 {
|
|
s = s[:80] + "..."
|
|
}
|
|
parts = append(parts, html.EscapeString(fmt.Sprintf("%s: %s", k, s)))
|
|
}
|
|
result := strings.Join(parts, ", ")
|
|
if len(result) > 300 {
|
|
result = result[:300] + "..."
|
|
}
|
|
return result
|
|
}
|
|
|
|
// FragmentSystemMessage returns an HTML fragment for a system message.
|
|
func FragmentSystemMessage(text string) string {
|
|
escaped := html.EscapeString(text)
|
|
return fmt.Sprintf(`<div id="chat-messages" hx-swap-oob="beforeend"><div class="message message-system">%s</div></div>`, escaped)
|
|
}
|
|
|
|
// FragmentTypingIndicator shows or hides the typing indicator.
|
|
func FragmentTypingIndicator(show bool) string {
|
|
if show {
|
|
return `<div id="typing-indicator" hx-swap-oob="innerHTML"><span class="typing-indicator">Claude razmišlja<span class="dots">...</span></span></div>`
|
|
}
|
|
return `<div id="typing-indicator" hx-swap-oob="innerHTML"></div>`
|
|
}
|
|
|
|
// FragmentStatus updates the connection status indicator.
|
|
func FragmentStatus(connected bool) string {
|
|
if connected {
|
|
return `<span id="ws-status" hx-swap-oob="innerHTML" class="status connected">Povezan</span>`
|
|
}
|
|
return `<span id="ws-status" hx-swap-oob="innerHTML" class="status">Nepovezan</span>`
|
|
}
|
|
|
|
// FragmentClearInput clears the message input field.
|
|
func FragmentClearInput() string {
|
|
return `<textarea id="message-input" hx-swap-oob="outerHTML" name="message" class="chat-input" placeholder="Pošalji poruku..." rows="1"></textarea>`
|
|
}
|
|
|
|
// FragmentCombine joins multiple fragments into a single response.
|
|
func FragmentCombine(fragments ...string) string {
|
|
return strings.Join(fragments, "\n")
|
|
}
|