mirror of
https://github.com/coalaura/whiskr.git
synced 2025-09-09 17:29:54 +00:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f18e9e577e | ||
![]() |
ce9813a331 | ||
![]() |
aeed519df0 | ||
![]() |
a819ec7b38 | ||
![]() |
db138324fa | ||
![]() |
2d65c2f484 | ||
![]() |
d903521154 | ||
![]() |
30755706fb |
@@ -41,6 +41,8 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
- restrict model list (optionally allow all for authenticated users)
|
||||||
|
- make authentication optional (unless no allowed models)
|
||||||
- improved custom prompts
|
- improved custom prompts
|
||||||
- settings
|
- settings
|
||||||
- auto-retry on edit
|
- auto-retry on edit
|
||||||
|
12
chat.go
12
chat.go
@@ -276,6 +276,8 @@ func HandleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
for iteration := range raw.Iterations {
|
for iteration := range raw.Iterations {
|
||||||
debug("iteration %d of %d", iteration+1, raw.Iterations)
|
debug("iteration %d of %d", iteration+1, raw.Iterations)
|
||||||
|
|
||||||
|
response.Send(StartChunk())
|
||||||
|
|
||||||
if len(request.Tools) > 0 && iteration == raw.Iterations-1 {
|
if len(request.Tools) > 0 && iteration == raw.Iterations-1 {
|
||||||
debug("no more tool calls")
|
debug("no more tool calls")
|
||||||
|
|
||||||
@@ -341,6 +343,8 @@ func HandleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
tool.AsAssistantToolCall(message),
|
tool.AsAssistantToolCall(message),
|
||||||
tool.AsToolMessage(),
|
tool.AsToolMessage(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
response.Send(EndChunk())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,10 +358,12 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
id string
|
id string
|
||||||
result strings.Builder
|
|
||||||
tool *ToolCall
|
tool *ToolCall
|
||||||
)
|
)
|
||||||
|
|
||||||
|
buf := GetFreeBuffer()
|
||||||
|
defer pool.Put(buf)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
chunk, err := stream.Recv()
|
chunk, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -405,7 +411,7 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
|
|||||||
content := choice.Delta.Content
|
content := choice.Delta.Content
|
||||||
|
|
||||||
if content != "" {
|
if content != "" {
|
||||||
result.WriteString(content)
|
buf.WriteString(content)
|
||||||
|
|
||||||
response.Send(TextChunk(content))
|
response.Send(TextChunk(content))
|
||||||
} else if choice.Delta.Reasoning != nil {
|
} else if choice.Delta.Reasoning != nil {
|
||||||
@@ -413,7 +419,7 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tool, result.String(), nil
|
return tool, buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SplitImagePairs(text string) []openrouter.ChatMessagePart {
|
func SplitImagePairs(text string) []openrouter.ChatMessagePart {
|
||||||
|
8
exa.go
8
exa.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,13 +31,14 @@ type ExaResults struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExaResults) String() string {
|
func (e *ExaResults) String() string {
|
||||||
var builder strings.Builder
|
buf := GetFreeBuffer()
|
||||||
|
defer pool.Put(buf)
|
||||||
|
|
||||||
json.NewEncoder(&builder).Encode(map[string]any{
|
json.NewEncoder(buf).Encode(map[string]any{
|
||||||
"results": e.Results,
|
"results": e.Results,
|
||||||
})
|
})
|
||||||
|
|
||||||
return builder.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExaRequest(ctx context.Context, path string, data any) (*http.Request, error) {
|
func NewExaRequest(ctx context.Context, path string, data any) (*http.Request, error) {
|
||||||
|
29
github.go
29
github.go
@@ -220,35 +220,36 @@ func RepoOverview(ctx context.Context, arguments GitHubRepositoryArguments) (str
|
|||||||
// wait and combine results
|
// wait and combine results
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
var builder strings.Builder
|
buf := GetFreeBuffer()
|
||||||
|
defer pool.Put(buf)
|
||||||
|
|
||||||
fmt.Fprintf(&builder, "### %s (%s)\n", repository.Name, repository.Visibility)
|
fmt.Fprintf(buf, "### %s (%s)\n", repository.Name, repository.Visibility)
|
||||||
fmt.Fprintf(&builder, "- URL: %s\n", repository.HtmlURL)
|
fmt.Fprintf(buf, "- URL: %s\n", repository.HtmlURL)
|
||||||
fmt.Fprintf(&builder, "- Description: %s\n", strings.ReplaceAll(repository.Description, "\n", " "))
|
fmt.Fprintf(buf, "- Description: %s\n", strings.ReplaceAll(repository.Description, "\n", " "))
|
||||||
fmt.Fprintf(&builder, "- Default branch: %s\n", repository.DefaultBranch)
|
fmt.Fprintf(buf, "- Default branch: %s\n", repository.DefaultBranch)
|
||||||
fmt.Fprintf(&builder, "- Stars: %d | Forks: %d\n", repository.Stargazers, repository.Forks)
|
fmt.Fprintf(buf, "- Stars: %d | Forks: %d\n", repository.Stargazers, repository.Forks)
|
||||||
|
|
||||||
builder.WriteString("\n### Top-level files and directories\n")
|
buf.WriteString("\n### Top-level files and directories\n")
|
||||||
|
|
||||||
if len(directories) == 0 && len(files) == 0 {
|
if len(directories) == 0 && len(files) == 0 {
|
||||||
builder.WriteString("*No entries or insufficient permissions.*\n")
|
buf.WriteString("*No entries or insufficient permissions.*\n")
|
||||||
} else {
|
} else {
|
||||||
for _, directory := range directories {
|
for _, directory := range directories {
|
||||||
fmt.Fprintf(&builder, "- [D] %s\n", directory)
|
fmt.Fprintf(buf, "- [D] %s\n", directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
fmt.Fprintf(&builder, "- [F] %s\n", file)
|
fmt.Fprintf(buf, "- [F] %s\n", file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.WriteString("\n### README\n")
|
buf.WriteString("\n### README\n")
|
||||||
|
|
||||||
if readmeMarkdown == "" {
|
if readmeMarkdown == "" {
|
||||||
builder.WriteString("*No README found or could not load.*\n")
|
buf.WriteString("*No README found or could not load.*\n")
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString(readmeMarkdown)
|
buf.WriteString(readmeMarkdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.String(), nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
8
go.mod
8
go.mod
@@ -6,8 +6,8 @@ require (
|
|||||||
github.com/coalaura/plain v0.2.0
|
github.com/coalaura/plain v0.2.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/revrost/go-openrouter v0.2.2
|
github.com/revrost/go-openrouter v0.2.3
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.42.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -16,6 +16,6 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/rs/zerolog v1.34.0 // indirect
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/term v0.34.0 // indirect
|
golang.org/x/term v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
10
go.sum
10
go.sum
@@ -7,8 +7,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
|
||||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
|
||||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
@@ -26,6 +24,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/revrost/go-openrouter v0.2.2 h1:7bOdLPKmw0iJB1AdpN+YaWUd2XC9cwfJKDY10iaSAzI=
|
github.com/revrost/go-openrouter v0.2.2 h1:7bOdLPKmw0iJB1AdpN+YaWUd2XC9cwfJKDY10iaSAzI=
|
||||||
github.com/revrost/go-openrouter v0.2.2/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
|
github.com/revrost/go-openrouter v0.2.2/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
|
||||||
|
github.com/revrost/go-openrouter v0.2.3 h1:ollIaPrgVWgqJyKbJGSX1jFs66eAWJs8Ojrxnd2i/E0=
|
||||||
|
github.com/revrost/go-openrouter v0.2.3/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
@@ -33,13 +33,19 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
|
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||||
|
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@@ -134,9 +134,10 @@ func BuildPrompt(name string, metadata Metadata, model *Model) (string, error) {
|
|||||||
metadata.Platform = "Unknown"
|
metadata.Platform = "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
buf := GetFreeBuffer()
|
||||||
|
defer pool.Put(buf)
|
||||||
|
|
||||||
err := tmpl.Execute(&buf, PromptData{
|
err := tmpl.Execute(buf, PromptData{
|
||||||
Name: model.Name,
|
Name: model.Name,
|
||||||
Slug: model.ID,
|
Slug: model.ID,
|
||||||
Date: time.Now().In(tz).Format(time.RFC1123),
|
Date: time.Now().In(tz).Format(time.RFC1123),
|
||||||
|
10
search.go
10
search.go
@@ -210,12 +210,18 @@ func ParseAndUpdateArgs(tool *ToolCall, arguments any) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(arguments)
|
buf := GetFreeBuffer()
|
||||||
|
defer pool.Put(buf)
|
||||||
|
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
|
||||||
|
err = enc.Encode(arguments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tool.Args = string(b)
|
tool.Args = buf.String()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -762,6 +762,7 @@ body:not(.loading) #loading {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.files {
|
.files {
|
||||||
|
@@ -91,10 +91,26 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown .table-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 16px 0;
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown .table-wrapper:not(.overflowing) {
|
||||||
|
cursor: default;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown .table-wrapper.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown table {
|
.markdown table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin: 16px 0;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -860,6 +860,8 @@
|
|||||||
|
|
||||||
messages.splice(index, 1);
|
messages.splice(index, 1);
|
||||||
|
|
||||||
|
setFollowTail(distanceFromBottom() <= nearBottom);
|
||||||
|
|
||||||
this.#save();
|
this.#save();
|
||||||
|
|
||||||
$messages.dispatchEvent(new Event("scroll"));
|
$messages.dispatchEvent(new Event("scroll"));
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
(() => {
|
(() => {
|
||||||
const timeouts = new WeakMap();
|
const timeouts = new WeakMap(),
|
||||||
|
scrollState = {
|
||||||
|
el: null,
|
||||||
|
startX: 0,
|
||||||
|
scrollLeft: 0,
|
||||||
|
pointerId: null,
|
||||||
|
moved: false,
|
||||||
|
};
|
||||||
|
|
||||||
marked.use({
|
marked.use({
|
||||||
async: false,
|
async: false,
|
||||||
@@ -7,13 +14,13 @@
|
|||||||
gfm: true,
|
gfm: true,
|
||||||
pedantic: false,
|
pedantic: false,
|
||||||
|
|
||||||
walkTokens: (token) => {
|
walkTokens: token => {
|
||||||
const { type, text } = token;
|
const { type, text } = token;
|
||||||
|
|
||||||
if (type === "html") {
|
if (type === "html") {
|
||||||
token.text = token.text.replace(/&/g, "&")
|
token.text = token.text.replace(/&/g, "&");
|
||||||
token.text = token.text.replace(/</g, "<")
|
token.text = token.text.replace(/</g, "<");
|
||||||
token.text = token.text.replace(/>/g, ">")
|
token.text = token.text.replace(/>/g, ">");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else if (type !== "code") {
|
} else if (type !== "code") {
|
||||||
@@ -48,9 +55,18 @@
|
|||||||
return `<a href="${link.href}" target="_blank">${escapeHtml(link.text || link.href)}</a>`;
|
return `<a href="${link.href}" target="_blank">${escapeHtml(link.text || link.href)}</a>`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hooks: {
|
||||||
|
postprocess: html => {
|
||||||
|
html = html.replace(/<table>/g, `<div class="table-wrapper"><table>`);
|
||||||
|
html = html.replace(/<\/ ?table>/g, `</table></div>`);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener("click", (event) => {
|
addEventListener("click", event => {
|
||||||
const button = event.target,
|
const button = event.target,
|
||||||
header = button.closest(".pre-header"),
|
header = button.closest(".pre-header"),
|
||||||
pre = header?.closest("pre"),
|
pre = header?.closest("pre"),
|
||||||
@@ -70,11 +86,76 @@
|
|||||||
pre,
|
pre,
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.classList.remove("copied");
|
button.classList.remove("copied");
|
||||||
}, 1000),
|
}, 1000)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.render = (markdown) => {
|
addEventListener("pointerover", event => {
|
||||||
|
if (event.pointerType !== "mouse") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = event.target.closest(".table-wrapper");
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.classList.toggle("overflowing", el.scrollWidth - el.clientWidth > 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
addEventListener("pointerdown", event => {
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = event.target.closest(".table-wrapper");
|
||||||
|
|
||||||
|
if (!el || !el.classList.contains("overflowing")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollState.el = el;
|
||||||
|
scrollState.pointerId = event.pointerId;
|
||||||
|
scrollState.startX = event.clientX;
|
||||||
|
scrollState.scrollLeft = el.scrollLeft;
|
||||||
|
scrollState.moved = false;
|
||||||
|
|
||||||
|
el.classList.add("dragging");
|
||||||
|
el.setPointerCapture?.(event.pointerId);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
addEventListener("pointermove", event => {
|
||||||
|
if (!scrollState.el || event.pointerId !== scrollState.pointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = event.clientX - scrollState.startX;
|
||||||
|
|
||||||
|
if (Math.abs(dx) > 3) {
|
||||||
|
scrollState.moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollState.el.scrollLeft = scrollState.scrollLeft - dx;
|
||||||
|
});
|
||||||
|
|
||||||
|
function endScroll(event) {
|
||||||
|
if (!scrollState.el || (event && event.pointerId !== scrollState.pointerId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollState.el.classList.remove("dragging");
|
||||||
|
scrollState.el.releasePointerCapture?.(scrollState.pointerId);
|
||||||
|
scrollState.el = null;
|
||||||
|
scrollState.pointerId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener("pointerup", endScroll);
|
||||||
|
addEventListener("pointercancel", endScroll);
|
||||||
|
|
||||||
|
window.render = markdown => {
|
||||||
return marked.parse(markdown);
|
return marked.parse(markdown);
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
27
stream.go
27
stream.go
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
type Chunk struct {
|
type Chunk struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Text any `json:"text"`
|
Text any `json:"text,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
@@ -27,6 +27,14 @@ var pool = sync.Pool{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFreeBuffer() *bytes.Buffer {
|
||||||
|
buf := pool.Get().(*bytes.Buffer)
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
func NewStream(w http.ResponseWriter, ctx context.Context) (*Stream, error) {
|
func NewStream(w http.ResponseWriter, ctx context.Context) (*Stream, error) {
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
@@ -72,6 +80,18 @@ func IDChunk(id string) Chunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EndChunk() Chunk {
|
||||||
|
return Chunk{
|
||||||
|
Type: "end",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartChunk() Chunk {
|
||||||
|
return Chunk{
|
||||||
|
Type: "start",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ErrorChunk(err error) Chunk {
|
func ErrorChunk(err error) Chunk {
|
||||||
return Chunk{
|
return Chunk{
|
||||||
Type: "error",
|
Type: "error",
|
||||||
@@ -92,10 +112,7 @@ func WriteChunk(w http.ResponseWriter, ctx context.Context, chunk any) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := pool.Get().(*bytes.Buffer)
|
buf := GetFreeBuffer()
|
||||||
|
|
||||||
buf.Reset()
|
|
||||||
|
|
||||||
defer pool.Put(buf)
|
defer pool.Put(buf)
|
||||||
|
|
||||||
if err := json.NewEncoder(buf).Encode(chunk); err != nil {
|
if err := json.NewEncoder(buf).Encode(chunk); err != nil {
|
||||||
|
7
title.go
7
title.go
@@ -82,9 +82,10 @@ func HandleTitle(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var prompt strings.Builder
|
buf := GetFreeBuffer()
|
||||||
|
defer pool.Put(buf)
|
||||||
|
|
||||||
if err := InternalTitleTmpl.Execute(&prompt, raw); err != nil {
|
if err := InternalTitleTmpl.Execute(buf, raw); err != nil {
|
||||||
RespondJson(w, http.StatusInternalServerError, map[string]any{
|
RespondJson(w, http.StatusInternalServerError, map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
@@ -95,7 +96,7 @@ func HandleTitle(w http.ResponseWriter, r *http.Request) {
|
|||||||
request := openrouter.ChatCompletionRequest{
|
request := openrouter.ChatCompletionRequest{
|
||||||
Model: env.Settings.TitleModel,
|
Model: env.Settings.TitleModel,
|
||||||
Messages: []openrouter.ChatCompletionMessage{
|
Messages: []openrouter.ChatCompletionMessage{
|
||||||
openrouter.SystemMessage(prompt.String()),
|
openrouter.SystemMessage(buf.String()),
|
||||||
openrouter.UserMessage(strings.Join(messages, "\n")),
|
openrouter.UserMessage(strings.Join(messages, "\n")),
|
||||||
},
|
},
|
||||||
Temperature: 0.25,
|
Temperature: 0.25,
|
||||||
|
Reference in New Issue
Block a user