From 40223a12b542bd58f7f73ff0d10a4ccbfa90d5e0 Mon Sep 17 00:00:00 2001 From: Chandler Swift Date: Sun, 28 Dec 2025 19:42:39 -0600 Subject: [PATCH] Add testimonials --- cmd/test_provider/test_provider.go | 69 ++++++++ completion/llama-server/llama-server.go | 3 +- completion/openrouter/openrouter.go | 27 ++- completion/provider.go | 2 +- main.go | 20 ++- site/site.go | 82 ++++++--- templates/index.html | 216 +++++++++++++++++------- 7 files changed, 318 insertions(+), 101 deletions(-) create mode 100644 cmd/test_provider/test_provider.go diff --git a/cmd/test_provider/test_provider.go b/cmd/test_provider/test_provider.go new file mode 100644 index 0000000..fefe749 --- /dev/null +++ b/cmd/test_provider/test_provider.go @@ -0,0 +1,69 @@ +// this package provides a wrapper to test completion provider implementations. +package main + +import ( + "context" + "fmt" + "os" + + "git.chandlerswift.com/chandlerswift/svs-services-server/completion/openrouter" +) + +var provider = openrouter.OpenRouterProvider{ + Token: os.Getenv("OPENROUTER_API_KEY"), + Model: "openai/gpt-oss-120b", +} + +func main() { + prompt := "Generate a sample weather report." + + // { + // name: 'weather', + // strict: true, + // schema: { + // type: 'object', + // properties: { + // location: { + // type: 'string', + // description: 'City or location name', + // }, + // temperature: { + // type: 'number', + // description: 'Temperature in Celsius', + // }, + // conditions: { + // type: 'string', + // description: 'Weather conditions description', + // }, + // }, + // required: ['location', 'temperature', 'conditions'], + // additionalProperties: false, + // }, + // }, + // } + schema := map[string]any{ + "name": "weather", + "strict": true, + "schema": map[string]any{ + "type": "object", + "properties": map[string]any{ + "location": map[string]any{ + "type": "string", + "description": "City or location name", + }, + "temperature": map[string]any{ + "type": "number", + "description": "Temperature in Celsius", + }, + "conditions": map[string]any{ + "type": "string", + "description": "Weather conditions description", + }, + }, + "required": []string{"location", "temperature", "conditions"}, + "additionalProperties": false, + }, + } + result, err := provider.Complete(context.TODO(), prompt, schema) + fmt.Println(err, result) +} diff --git a/completion/llama-server/llama-server.go b/completion/llama-server/llama-server.go index e342a9c..3e96ff2 100644 --- a/completion/llama-server/llama-server.go +++ b/completion/llama-server/llama-server.go @@ -71,7 +71,8 @@ func (p LlamaServerProvider) Health() (err error) { return nil } -func (p LlamaServerProvider) Complete(ctx context.Context, prompt string) (response string, err error) { +func (p LlamaServerProvider) Complete(ctx context.Context, prompt string, schema any) (response string, err error) { + // TODO: schema enforcement req := Request{ Messages: []Message{ { diff --git a/completion/openrouter/openrouter.go b/completion/openrouter/openrouter.go index f070208..cf73a69 100644 --- a/completion/openrouter/openrouter.go +++ b/completion/openrouter/openrouter.go @@ -22,16 +22,13 @@ type Message struct { } type ChatCompletionRequest struct { - Model string `json:"model"` - Messages []Message `json:"messages"` - Temperature *float64 `json:"temperature,omitempty"` - MaxTokens *int `json:"max_tokens,omitempty"` - TopP *float64 `json:"top_p,omitempty"` - Stop json.RawMessage `json:"stop,omitempty"` // string or []string; keep flexible - Provider struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + Provider struct { Sort string `json:"sort,omitempty"` } `json:"provider,omitempty"` - + ResponseFormat any `json:"response_format,omitempty"` // keep flexible + Plugins []map[string]string `json:"plugins,omitempty"` } type ChatCompletionResponse struct { @@ -46,7 +43,7 @@ type ChatCompletionResponse struct { } `json:"choices"` } -func (p OpenRouterProvider) Complete(ctx context.Context, prompt string) (string, error) { +func (p OpenRouterProvider) Complete(ctx context.Context, prompt string, schema any) (string, error) { req := ChatCompletionRequest{ Model: p.Model, Messages: []Message{ @@ -62,6 +59,18 @@ func (p OpenRouterProvider) Complete(ctx context.Context, prompt string) (string }, } + if schema != nil { + req.ResponseFormat = map[string]any{ + "type": "json_schema", + "json_schema": schema, + } + req.Plugins = []map[string]string{ + { + "id": "response-healing", + }, + } + } + httpClient := http.Client{Timeout: 10 * time.Second} body, err := json.Marshal(req) if err != nil { diff --git a/completion/provider.go b/completion/provider.go index f7ed85a..42ffa48 100644 --- a/completion/provider.go +++ b/completion/provider.go @@ -3,5 +3,5 @@ package completion import "context" type CompletionProvider interface { - Complete(ctx context.Context, prompt string) (string, error) + Complete(ctx context.Context, prompt string, schema any) (string, error) } diff --git a/main.go b/main.go index fd5580b..0457354 100644 --- a/main.go +++ b/main.go @@ -26,8 +26,24 @@ var notFoundTemplate string //go:embed headshots/* var headshots embed.FS -var tmpl = template.Must(template.New("index").Parse(indexTemplate)) -var notFoundTmpl = template.Must(template.New("404").Parse(notFoundTemplate)) +// Used for testimonial slider +var funcMap = template.FuncMap{ + "add": func(a, b int) int { + return a + b + }, + "sub": func(a, b int) int { + return a - b + }, + "mod": func(a, b int) int { + if b == 0 { + return 0 + } + return a % b + }, +} + +var tmpl = template.Must(template.New("index").Funcs(funcMap).Parse(indexTemplate)) +var notFoundTmpl = template.Must(template.New("404").Funcs(funcMap).Parse(notFoundTemplate)) var completionProviders map[string]completion.CompletionProvider = map[string]completion.CompletionProvider{ "openrouter": openrouter.OpenRouterProvider{ diff --git a/site/site.go b/site/site.go index 37a6a95..94547e9 100644 --- a/site/site.go +++ b/site/site.go @@ -93,7 +93,7 @@ func GenerateSiteData(rawservice string, cp completion.CompletionProvider) (site prompt = strings.TrimSpace(prompt) prompt = whitespace.ReplaceAllString(prompt, " ") - site.Occupation, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, rawservice)) + site.Occupation, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, rawservice), nil) if err != nil { return nil, fmt.Errorf("parsing service: %w", err) } @@ -104,7 +104,7 @@ func GenerateSiteData(rawservice string, cp completion.CompletionProvider) (site e.g. for a web design company, something like "Custom websites cooked to order". Do not include quotes. `) - site.Tagline, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + site.Tagline, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation), nil) if err != nil { return nil, fmt.Errorf("generating tagline: %w", err) } @@ -129,7 +129,7 @@ func GenerateSiteData(rawservice string, cp completion.CompletionProvider) (site Markup should be in HTML, including paragraph breaks. Do not wrap in any additional markup. `) - whatWeDo, err := cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + whatWeDo, err := cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation), nil) site.WhatWeDo = template.HTML(whatWeDo) if err != nil { return nil, fmt.Errorf("generating what we do: %w", err) @@ -150,7 +150,7 @@ func GenerateSiteData(rawservice string, cp completion.CompletionProvider) (site Emphasize the relevance of all the above information to SVS's line of work. Do not shoehorn all the above information; only include each piece if relevant. `) - bio, err := cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + bio, err := cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation), nil) if err != nil { return nil, fmt.Errorf("generating Chandler bio: %w", err) } @@ -171,11 +171,11 @@ func GenerateSiteData(rawservice string, cp completion.CompletionProvider) (site Emphasize the relevance of all the above information to SVS's line of work. Do not include all the above information; only mention each piece if relevant. `) - bio, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + bio, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation), nil) if err != nil { - return nil, fmt.Errorf("generating Isaac bio: %w", err) + return nil, fmt.Errorf("generating Eric bio: %w", err) } - site.EricBio = template.HTML(fmt.Sprintf("Eric is a seasoned professional in the %v industry, known for his expertise and dedication.", site.Occupation)) + site.EricBio = template.HTML(bio) prompt = norm(` Generate a short, satirical/irreverent bio (2-4 sentences) for Isaac Swift, a co-founder of SVS, a %v company. @@ -195,31 +195,63 @@ func GenerateSiteData(rawservice string, cp completion.CompletionProvider) (site Emphasize the relevance of all the above information to SVS's line of work. Do not include all the above information; only mention each piece if relevant. `) - bio, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + bio, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation), nil) if err != nil { return nil, fmt.Errorf("generating Isaac bio: %w", err) } site.IsaacBio = template.HTML(bio) - - - site.Testimonials = []Testimonial{ - { - Name: "Barack Obama", - Position: "Poolhall acquaintance", - Quote: "After the fiasco of the ACA Website, I know how important it is to get a website right on the first try, and boy do these gentlemen deliver!", - }, - { - Name: "Ada Lovelace", - Position: "Pioneer of Computing", - Quote: "The elegance and functionality of the websites created by SVS Web Design are truly ahead of their time.", - }, - { - Name: "Steve Jobs", - Position: "Visionary Entrepreneur", - Quote: "Design is not just what it looks like and feels like. Design is how it works. SVS Web Design understands this principle deeply.", + prompt = norm(` + Generate three satirical testimonials from satisfied clients of SVS, a %v company. + Testimonials should be from well-known figures, possibly historical (say, a former US President). + When possible, the person quoted should have some tenuous connection to the services provided by SVS. + This may be heavily ironic, say Abe Lincoln giving a testimonial for SVS's web design services, or Helen Keller praising their audio engineering. + Other possible sources include George Washington, Thomas Edison, Nikola Tesla, Marie Curie, Albert Einstein, Winston Churchill, Franklin D. Roosevelt, John F. Kennedy, Ronald Reagan, Margaret Thatcher, and Martin Luther King Jr. + Each testimonial should be 1-2 sentences long, and include an author and their position. + For example, if SVS did web design, a testimonial might be from "Barack Obama", a "Poolhall acquaintance": + "After the fiasco of the ACA Website, I know how important it is to get a website right on the first try, + and boy do these gentlemen deliver!" + Do not include quotes around the testimonial text. + Return the testimonials in JSON format, as an array of objects with "name", "position", and "quote" properties. + `) + schema := map[string]any{ + "name": "testimonials", + "strict": true, + "schema": map[string]any{ + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "Name": map[string]any{ + "type": "string", + "description": "Name of the person giving the testimonial", + }, + "Position": map[string]any{ + "type": "string", + "description": "Position or title of the person giving the testimonial", + }, + "Quote": map[string]any{ + "type": "string", + "description": "Testimonial text", + }, + }, + "required": []string{"Name", "Position", "Quote"}, + "additionalProperties": false, + }, }, } + raw_testimonials, err := cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation), schema) + if err != nil { + return nil, fmt.Errorf("generating testimonials: %w", err) + } + fmt.Println("Raw testimonials: %s\n", raw_testimonials) + err = json.Unmarshal([]byte(raw_testimonials), &site.Testimonials) + if err != nil { + return nil, fmt.Errorf("unmarshaling testimonials: %w", err) + } + site.CurrentYear = time.Now().Year() return } diff --git a/templates/index.html b/templates/index.html index 3cd214b..74e5427 100644 --- a/templates/index.html +++ b/templates/index.html @@ -22,6 +22,10 @@ box-sizing: border-box; } + html { + scroll-behavior: smooth; + } + body { margin: 0; font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; @@ -54,13 +58,14 @@ } header h1 { - margin: 0 0 10px; - font-size: clamp(2.2rem, 3vw, 3.4rem); + margin: 0; + font-size: clamp(2.2rem, 5vw, 5rem); letter-spacing: 0.5px; } header h2 { margin: 0 0 10px; + font-size: clamp(2.2rem, 3vw, 3rem); font-weight: 600; } @@ -107,6 +112,12 @@ color: var(--muted); } + .section-lead { + text-align: center; + margin: 0 auto; + max-width: 700px; + } + #what-we-do { background: #eef2ff; } @@ -179,23 +190,31 @@ .slides { position: relative; overflow: hidden; - min-height: 180px; + min-height: 200px; + } + + @property --auto { + syntax: ''; + inherits: false; + initial-value: 0; + } + + .slides-track { + display: flex; + width: calc(100% * var(--count)); + transform: translateX(calc(-33.3333% * var(--current, 0))); + transition: transform 320ms ease; + animation: auto-pan calc(var(--count) * 6s) infinite linear; + } + + .slide-toggle { + display: none; } .slide { - position: absolute; - inset: 0; - opacity: 0; - transform: translateX(20px); - transition: opacity 250ms ease, transform 250ms ease; + flex: 0 0 calc(100% / var(--count)); + padding: 16px; text-align: center; - padding: 10px 16px; - } - - .slide.active { - opacity: 1; - transform: translateX(0); - position: relative; } .slide p { @@ -207,11 +226,31 @@ .controls { display: flex; justify-content: center; + align-items: center; gap: 12px; margin-top: 20px; } - .control-btn { + .control-dot { + width: 10px; + height: 10px; + border-radius: 50%; + border: 1px solid #c7cedc; + background: #fff; + cursor: pointer; + transition: transform 150ms ease, background 150ms ease, border-color 150ms ease; + } + + .control-dot:hover { + transform: scale(1.1); + background: #eef2ff; + } + + .slides:has(.slide-toggle:checked) .slides-track { + animation-play-state: paused; + } + + .control-arrow { width: 42px; height: 42px; border-radius: 50%; @@ -224,16 +263,83 @@ box-shadow: var(--shadow); } - .control-btn:hover { + .control-arrow:hover { background: #eef2ff; } + .arrow-sets { + position: absolute; + inset: 0; + pointer-events: none; + } + + .arrow-set { + display: none; + position: absolute; + inset: 0; + padding: 0 8px; + align-items: center; + justify-content: space-between; + pointer-events: none; + } + + .arrow-set:first-of-type { + display: flex; + } + + @keyframes auto-pan { + 0% { + --auto: 0; + } + 100% { + --auto: calc(var(--count) - 1); + } + } + + /* Active state wiring via :has so dots/arrows track selection. */ + .slides { + --current: var(--auto); + } + + /* Default to first dot when nothing is picked. */ + .controls .control-dot:first-of-type { + background: #eef2ff; + border-color: #c7cedc; + } + + .arrow-set .control-arrow { + pointer-events: auto; + } + + {{- range $index, $_ := .Testimonials }} + .slides:has(#t-{{$index}}:checked) { + --current: {{$index}}; + } + + .slides:has(#t-{{$index}}:checked) .controls .control-dot[for="t-{{$index}}"] { + background: var(--accent); + border-color: var(--accent); + } + + .slides:has(#t-{{$index}}:checked) .arrow-set.arrows-{{$index}} { + display: flex; + } + {{- end }} + footer { background: #0b1224; color: #f1f5f9; padding: 40px 0; } + footer a { + color: #cbd5e1; + } + + footer a:hover { + color: #fff; + } + footer .footer-inner { display: flex; flex-direction: column; @@ -274,7 +380,7 @@

About Us

-

Three builders, one vision: ship reliable, human-centered software and services.

+

Three builders, one vision: ship reliable, human-centered software and services.

What Our Clients Say

-
-
- {{range $index, $testimonial := .Testimonials}} -
-

"{{$testimonial.Quote}}"

- — {{$testimonial.Name}}, {{$testimonial.Position}} + {{- $count := len .Testimonials -}} +
+
+
+ {{range $index, $testimonial := .Testimonials}} + +
+

"{{$testimonial.Quote}}"

+ — {{$testimonial.Name}}, {{$testimonial.Position}} +
+ {{end}} +
+
+ {{range $index, $_ := .Testimonials}} + {{- $prev := mod (add $index (sub $count 1)) $count -}} + {{- $next := mod (add $index 1) $count -}} +
+ + +
+ {{end}} +
+
+ {{range $index, $_ := .Testimonials}} + + {{end}}
- {{end}} -
-
- -
@@ -325,43 +446,12 @@

Swift, Villnow & Swift Industries
{{.Occupation}} Division
- (218) 210-5180
- sales@svsindustries.org + (218) 210-5180
+ sales@svsindustries.org

-

© {{.CurrentYear}} Swift, Villnow & Swift Industries. All rights reserved.

+

© {{.CurrentYear}} Swift, Villnow & Swift Industries
All rights reserved.

- -