Skip to content

Instantly share code, notes, and snippets.

@leoshimo
Created January 2, 2025 04:34
Show Gist options
  • Save leoshimo/d3db5d37d348739e8119b73c730d7a6d to your computer and use it in GitHub Desktop.
Save leoshimo/d3db5d37d348739e8119b73c730d7a6d to your computer and use it in GitHub Desktop.
#!/usr/bin/env wish
package require http
package require tls
package require json
package require json::write
# Initialize TLS
::tls::init -ssl2 0 -ssl3 0 -tls1 0 -tls1.1 0 -tls1.2 1 -tls1.3 1
# Configure HTTPS with proper TLS settings
::http::register https 443 [list ::tls::socket \
-autoservername true \
-request 1 \
-require 0 \
-ssl2 0 \
-ssl3 0 \
-tls1 0 \
-tls1.1 0 \
-tls1.2 1 \
-tls1.3 1]
# Global variables
set apiKey ""
set messageHistory ""
set baseUrl "https://api.anthropic.com/v1/messages"
set currentToken ""
# Parse command line arguments
if {$argc > 0} {
for {set i 0} {$i < $argc} {incr i} {
set arg [lindex $argv $i]
if {$arg eq "--api-key"} {
incr i
if {$i < $argc} {
set apiKey [lindex $argv $i]
}
}
}
}
# Create main window
wm title . "Claude Chat Client"
wm minsize . 600 400
# Create API key entry frame
ttk::frame .apiFrame
ttk::label .apiFrame.label -text "API Key: "
ttk::entry .apiFrame.entry -show "*" -width 50
ttk::button .apiFrame.save -text "Save" -command {
set apiKey [.apiFrame.entry get]
.apiFrame.status configure -text "READY"
}
ttk::label .apiFrame.status -text ""
# If API key was provided via command line, populate the entry
if {$apiKey ne ""} {
.apiFrame.entry insert 0 $apiKey
.apiFrame.status configure -text "READY"
}
pack .apiFrame -fill x -pady 5 -padx 5
pack .apiFrame.label .apiFrame.entry .apiFrame.save .apiFrame.status -side left -padx 2
# Create chat display
text .chatDisplay -wrap word -yscrollcommand {.scroll set} -width 70 -height 20
scrollbar .scroll -command {.chatDisplay yview}
pack .scroll -side right -fill y
pack .chatDisplay -fill both -expand 1 -padx 5 -pady 5
# Create input frame
ttk::frame .inputFrame
text .inputFrame.entry -wrap word -width 60 -height 4
ttk::button .inputFrame.send -text "Send" -command sendMessage
pack .inputFrame -fill x -pady 5 -padx 5
pack .inputFrame.entry -side left -fill both -expand 1 -padx 2
pack .inputFrame.send -side right -padx 2
# Process streaming response chunk
proc processChunk {token} {
# Get current data
if {[catch {
set data [::http::data $token]
# Process each line
foreach line [split $data \n] {
puts "Processing line: $line" ;# Debug output
if {[string match "data:*" $line]} {
set eventData [string trim [string range $line 5 end]]
puts "Event data: $eventData" ;# Debug output
if {$eventData ne "" && ![string match "*ping*" $eventData]} {
if {[catch {
set jsonData [json::json2dict $eventData]
puts "Parsed JSON: $jsonData" ;# Debug output
if {[dict exists $jsonData type]} {
set type [dict get $jsonData type]
puts "Event type: $type" ;# Debug output
switch $type {
"content_block_delta" {
if {[dict exists $jsonData delta] &&
[dict exists [dict get $jsonData delta] text]} {
set text [dict get [dict get $jsonData delta] text]
puts "Displaying text: $text" ;# Debug output
.chatDisplay insert end $text
.chatDisplay see end
update
}
}
"message_stop" {
puts "Message complete" ;# Debug output
.chatDisplay insert end "\n\n"
update
return
}
}
}
} err]} {
puts "JSON parsing error: $err" ;# Debug output
continue
}
}
}
}
# Force UI update
update
# Clear the processed data to prevent reprocessing
if {[string length $data] > 0} {
::http::data $token
}
# Continue processing if connection is still good
if {[::http::status $token] eq "ok"} {
after 10 [list processChunk $token]
} else {
::http::cleanup $token
}
} err]} {
puts "Error processing data: $err"
::http::cleanup $token
}
}
# HTTP request with streaming
proc httpPostStream {url headers data} {
if {[catch {
set token [::http::geturl $url \
-method POST \
-type "application/json" \
-headers $headers \
-query $data \
-timeout 300000]
processChunk $token
} err]} {
puts "Error sending request: $err"
.chatDisplay insert end "Error: $err\n" error
}
}
# Procedure to send message to Claude API
proc sendMessage {} {
global apiKey baseUrl messageHistory currentToken
if {$apiKey eq ""} {
.chatDisplay insert end "Please enter your API key first.\n" error
return
}
set userMessage [.inputFrame.entry get 1.0 end-1c]
if {$userMessage eq ""} return
# Display user message
.chatDisplay insert end "You: " user_prefix
.chatDisplay insert end "$userMessage\n\n"
# Clear input immediately
.inputFrame.entry delete 1.0 end
# Create request data
set messageObj [json::write object \
role [json::write string "user"] \
content [json::write string $userMessage]]
set messagesArray [json::write array $messageObj]
if {$messageHistory ne ""} {
set messagesArray [json::write array {*}$messageHistory $messageObj]
}
set requestData [json::write object \
model [json::write string "claude-3-sonnet-20240229"] \
messages $messagesArray \
max_tokens 1024 \
stream true]
# Prepare request headers
set headers [list \
anthropic-version "2023-06-01" \
x-api-key $apiKey \
content-type "application/json"]
# Display assistant prefix before streaming
.chatDisplay insert end "Claude: " assistant_prefix
# Send request
httpPostStream $baseUrl $headers $requestData
}
# Configure text tags for styling
.chatDisplay tag configure user_prefix -foreground blue -font {-weight bold}
.chatDisplay tag configure assistant_prefix -foreground green -font {-weight bold}
.chatDisplay tag configure error -foreground red
# Initial instructions
.chatDisplay insert end "Welcome to Claude Chat Client!\n"
.chatDisplay insert end "Please enter your API key above to begin.\n\n"
# Bind Return key to send message (Shift+Return for newline)
bind .inputFrame.entry <Return> {
if {![string match "*Shift*" %s]} {
sendMessage
break
}
}
@leoshimo
Copy link
Author

leoshimo commented Jan 2, 2025

1/1/24 - WIP

Roughly ~30 iterations on Claude 3.5 Sonnet before hitting usage limit.

Working Features

  • Streaming
  • API Key from CLI / UI
  • UI is usable, w/ Raw JSON previewer

Bugs:

Each request is clean slate, w.o previous messages:

    # Create request data
    set messageObj [json::write object \
        role [json::write string "user"] \
        content [json::write string $userMessage]]

Stop condition uses HTTP status, instead of stop reason:

        # Clear the processed data to prevent reprocessing
        if {[string length $data] > 0} {
            ::http::data $token
        }

Notes

Q/A: What about ChatGPT?

Claude is significantly better at Tcl/Tk than ChatGPT 4o out of the box. ChatGPT didn't get anywhere even after iterating quite a bit.

Pain Point: Claude partial file edits often wrong

Instead of regenerating entire artifact, Claude seems to sometimes perform partial edits. These often inserted extra brackets / malformed spacing. Asking to regenerate from scratch often produced better results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment