Created
January 2, 2025 04:34
-
-
Save leoshimo/d3db5d37d348739e8119b73c730d7a6d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
1/1/24 - WIP
Roughly ~30 iterations on Claude 3.5 Sonnet before hitting usage limit.
Working Features
Bugs:
Each request is clean slate, w.o previous messages:
Stop condition uses HTTP status, instead of stop reason:
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.