mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
playing with vdom/claude
This commit is contained in:
parent
77fbd324c9
commit
df61e2108a
308
cmd/wsh/cmd/wshcmd-aww.go
Normal file
308
cmd/wsh/cmd/wshcmd-aww.go
Normal file
@ -0,0 +1,308 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"image"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
|
||||
)
|
||||
|
||||
type RedditListing struct {
|
||||
Data struct {
|
||||
Children []struct {
|
||||
Data struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Permalink string `json:"permalink"`
|
||||
} `json:"data"`
|
||||
} `json:"children"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
var postPosition int
|
||||
|
||||
var awwCmd = &cobra.Command{
|
||||
Use: "aww [position]",
|
||||
Short: "show today's top r/aww image",
|
||||
RunE: awwRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
awwCmd.Flags().IntVarP(&postPosition, "position", "p", 0, "position of post (0-based)")
|
||||
rootCmd.AddCommand(awwCmd)
|
||||
}
|
||||
|
||||
func AwwStyleTag(ctx context.Context, props map[string]any) any {
|
||||
return vdom.Bind(`
|
||||
<style>
|
||||
.aww-container {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
margin: 20px 0;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-block;
|
||||
margin-top: 15px;
|
||||
color: #2980b9;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
background: #fdf0ed;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
func AwwContentTag(ctx context.Context, props map[string]any) any {
|
||||
isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool)
|
||||
errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string)
|
||||
imageData := GlobalVDomClient.GetAtomVal("imageData")
|
||||
title := GlobalVDomClient.GetAtomVal("title")
|
||||
postURL := GlobalVDomClient.GetAtomVal("postURL")
|
||||
|
||||
if isLoading {
|
||||
return vdom.Bind(`
|
||||
<div className="aww-container">
|
||||
<div className="loading">Finding today's cutest image...</div>
|
||||
</div>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
if errorMsg != "" {
|
||||
return vdom.Bind(`
|
||||
<div className="aww-container">
|
||||
<div className="error"><bindparam key="error"/></div>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"error": errorMsg,
|
||||
})
|
||||
}
|
||||
|
||||
return vdom.Bind(`
|
||||
<div className="aww-container">
|
||||
<div className="title"><bindparam key="title"/></div>
|
||||
<a href="#param:postURL" className="image-container">
|
||||
<img src="#param:imageData" alt="Cute animal of the day"/>
|
||||
</a>
|
||||
<a href="#param:postURL" className="link">View on Reddit</a>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"title": title,
|
||||
"imageData": imageData,
|
||||
"postURL": postURL,
|
||||
})
|
||||
}
|
||||
|
||||
// downloadImage downloads an image from URL and returns its bytes
|
||||
func downloadImage(url string) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// resizeImage resizes the image to be under 64KB
|
||||
func resizeImage(imgBytes []byte) (string, error) {
|
||||
// Decode image
|
||||
img, _, err := image.Decode(strings.NewReader(string(imgBytes)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Start with original size
|
||||
width := uint(img.Bounds().Dx())
|
||||
height := uint(img.Bounds().Dy())
|
||||
scaleFactor := 1.0
|
||||
|
||||
// Keep trying until we get under 64KB
|
||||
for {
|
||||
// Resize image
|
||||
newWidth := uint(float64(width) * scaleFactor)
|
||||
newHeight := uint(float64(height) * scaleFactor)
|
||||
resized := resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
|
||||
|
||||
// Encode to JPEG with quality 85
|
||||
var buf strings.Builder
|
||||
jpeg.Encode(base64.NewEncoder(base64.StdEncoding, &buf), resized, &jpeg.Options{Quality: 85})
|
||||
encoded := buf.String()
|
||||
|
||||
// Check if we're under 64KB
|
||||
if len(encoded) < 64*1024 {
|
||||
return fmt.Sprintf("data:image/jpeg;base64,%s", encoded), nil
|
||||
}
|
||||
|
||||
// Reduce size by 10% and try again
|
||||
scaleFactor *= 0.9
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTopAwwPost(position int) (string, string, string, error) {
|
||||
// Get a few extra posts in case some aren't images
|
||||
log.Printf("Fetching top %d posts from r/aww, pos:%d\n", position+5, position)
|
||||
limit := position + 5
|
||||
resp, err := http.Get(fmt.Sprintf("https://www.reddit.com/r/aww/top.json?t=day&limit=%d", limit))
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", "", fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
var listing RedditListing
|
||||
if err := json.NewDecoder(resp.Body).Decode(&listing); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
log.Printf("got listings num:%d\n", len(listing.Data.Children))
|
||||
|
||||
if len(listing.Data.Children) <= position {
|
||||
return "", "", "", fmt.Errorf("no post found at position %d", position)
|
||||
}
|
||||
|
||||
post := listing.Data.Children[position].Data
|
||||
return post.Title, post.URL, "https://reddit.com" + post.Permalink, nil
|
||||
}
|
||||
|
||||
func awwRun(cmd *cobra.Command, args []string) error {
|
||||
if postPosition < 0 {
|
||||
return fmt.Errorf("position must be non-negative")
|
||||
}
|
||||
|
||||
client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GlobalVDomClient = client
|
||||
|
||||
// Register components
|
||||
client.RegisterComponent("AwwStyleTag", AwwStyleTag)
|
||||
client.RegisterComponent("AwwContentTag", AwwContentTag)
|
||||
|
||||
// Initialize state
|
||||
client.SetAtomVal("isLoading", true)
|
||||
client.SetAtomVal("errorMsg", "")
|
||||
client.SetAtomVal("imageData", "")
|
||||
client.SetAtomVal("title", "")
|
||||
client.SetAtomVal("postURL", "")
|
||||
|
||||
// Set root element
|
||||
client.SetRootElem(vdom.Bind(`
|
||||
<div>
|
||||
<AwwStyleTag/>
|
||||
<AwwContentTag/>
|
||||
</div>
|
||||
`, nil))
|
||||
|
||||
// Create VDOM context
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start fetching the image
|
||||
go func() {
|
||||
// Fetch top post
|
||||
title, imageURL, postURL, err := fetchTopAwwPost(postPosition)
|
||||
if err != nil {
|
||||
client.SetAtomVal("errorMsg", fmt.Sprintf("Error fetching post: %v", err))
|
||||
client.SetAtomVal("isLoading", false)
|
||||
client.SendAsyncInitiation()
|
||||
return
|
||||
}
|
||||
|
||||
// Download image
|
||||
imgBytes, err := downloadImage(imageURL)
|
||||
if err != nil {
|
||||
client.SetAtomVal("errorMsg", fmt.Sprintf("Error downloading image: %v", err))
|
||||
client.SetAtomVal("isLoading", false)
|
||||
client.SendAsyncInitiation()
|
||||
return
|
||||
}
|
||||
|
||||
// Resize and encode image
|
||||
base64Image, err := resizeImage(imgBytes)
|
||||
if err != nil {
|
||||
client.SetAtomVal("errorMsg", fmt.Sprintf("Error processing image: %v", err))
|
||||
client.SetAtomVal("isLoading", false)
|
||||
client.SendAsyncInitiation()
|
||||
return
|
||||
}
|
||||
|
||||
// Update state with results
|
||||
client.SetAtomVal("title", title)
|
||||
client.SetAtomVal("imageData", base64Image)
|
||||
client.SetAtomVal("postURL", postURL)
|
||||
client.SetAtomVal("isLoading", false)
|
||||
client.SendAsyncInitiation()
|
||||
}()
|
||||
|
||||
<-client.DoneCh
|
||||
return nil
|
||||
}
|
258
cmd/wsh/cmd/wshcmd-ls.go
Normal file
258
cmd/wsh/cmd/wshcmd-ls.go
Normal file
@ -0,0 +1,258 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
|
||||
)
|
||||
|
||||
var lsCmd = &cobra.Command{
|
||||
Use: "ls [directory]",
|
||||
Short: "list directory contents with details",
|
||||
RunE: lsRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(lsCmd)
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Size int64
|
||||
Mode os.FileMode
|
||||
ModTime time.Time
|
||||
IsDir bool
|
||||
Extension string
|
||||
}
|
||||
|
||||
func LsStyleTag(ctx context.Context, props map[string]any) any {
|
||||
return vdom.Bind(`
|
||||
<style>
|
||||
.ls-container {
|
||||
padding: 20px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.ls-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.ls-header {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ls-row {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.ls-row:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.ls-cell {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.dir-name {
|
||||
color: #2980b9;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.size-cell {
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mode-cell {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
// formatSize converts bytes to human readable format
|
||||
func formatSize(size int64) string {
|
||||
if size < 1024 {
|
||||
return fmt.Sprintf("%d B", size)
|
||||
}
|
||||
units := []string{"B", "KB", "MB", "GB", "TB"}
|
||||
div := float64(1)
|
||||
unitIndex := 0
|
||||
for size/int64(div) >= 1024 && unitIndex < len(units)-1 {
|
||||
div *= 1024
|
||||
unitIndex++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %s", float64(size)/div, units[unitIndex])
|
||||
}
|
||||
|
||||
// formatMode formats file permissions in a Unix-like style
|
||||
func formatMode(mode os.FileMode) string {
|
||||
output := ""
|
||||
if mode.IsDir() {
|
||||
output += "d"
|
||||
} else {
|
||||
output += "-"
|
||||
}
|
||||
|
||||
// User permissions
|
||||
output += formatPerm(mode, 6)
|
||||
// Group permissions
|
||||
output += formatPerm(mode, 3)
|
||||
// Others permissions
|
||||
output += formatPerm(mode, 0)
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// formatPerm formats a permission triplet
|
||||
func formatPerm(mode os.FileMode, shift uint) string {
|
||||
output := ""
|
||||
output += map[bool]string{true: "r", false: "-"}[(mode>>(shift+2))&1 == 1]
|
||||
output += map[bool]string{true: "w", false: "-"}[(mode>>(shift+1))&1 == 1]
|
||||
output += map[bool]string{true: "x", false: "-"}[(mode>>shift)&1 == 1]
|
||||
return output
|
||||
}
|
||||
|
||||
func LsContentTag(ctx context.Context, props map[string]any) any {
|
||||
path := props["path"].(string)
|
||||
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return vdom.Bind(`
|
||||
<div className="error">Error reading directory: <bindparam key="error"/></div>
|
||||
`, map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Convert to FileInfo and sort
|
||||
files := make([]FileInfo, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, FileInfo{
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
Mode: info.Mode(),
|
||||
ModTime: info.ModTime(),
|
||||
IsDir: info.IsDir(),
|
||||
Extension: strings.ToLower(filepath.Ext(info.Name())),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort: directories first, then by name
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
if files[i].IsDir != files[j].IsDir {
|
||||
return files[i].IsDir
|
||||
}
|
||||
return files[i].Name < files[j].Name
|
||||
})
|
||||
|
||||
template := `
|
||||
<div className="ls-container">
|
||||
<table className="ls-table">
|
||||
<tr>
|
||||
<th className="ls-header">Mode</th>
|
||||
<th className="ls-header">Size</th>
|
||||
<th className="ls-header">Modified</th>
|
||||
<th className="ls-header">Name</th>
|
||||
</tr>`
|
||||
|
||||
for _, file := range files {
|
||||
nameClass := "file-name"
|
||||
if file.IsDir {
|
||||
nameClass = "dir-name"
|
||||
}
|
||||
|
||||
template += fmt.Sprintf(`
|
||||
<tr className="ls-row">
|
||||
<td className="ls-cell mode-cell">%s</td>
|
||||
<td className="ls-cell size-cell">%s</td>
|
||||
<td className="ls-cell time-cell">%s</td>
|
||||
<td className="ls-cell %s">%s</td>
|
||||
</tr>`,
|
||||
formatMode(file.Mode),
|
||||
formatSize(file.Size),
|
||||
file.ModTime.Format("Jan 02 15:04"),
|
||||
nameClass,
|
||||
file.Name)
|
||||
}
|
||||
|
||||
template += `
|
||||
</table>
|
||||
</div>`
|
||||
|
||||
return vdom.Bind(template, nil)
|
||||
}
|
||||
|
||||
func lsRun(cmd *cobra.Command, args []string) error {
|
||||
path := "."
|
||||
if len(args) > 0 {
|
||||
path = args[0]
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GlobalVDomClient = client
|
||||
|
||||
// Register components
|
||||
client.RegisterComponent("LsStyleTag", LsStyleTag)
|
||||
client.RegisterComponent("LsContentTag", LsContentTag)
|
||||
|
||||
// Set root element
|
||||
client.SetRootElem(vdom.Bind(`
|
||||
<div>
|
||||
<LsStyleTag/>
|
||||
<LsContentTag path="#param:path"/>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"path": absPath,
|
||||
}))
|
||||
|
||||
// Create VDOM context
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-client.DoneCh
|
||||
return nil
|
||||
}
|
320
cmd/wsh/cmd/wshcmd-rhyme.go
Normal file
320
cmd/wsh/cmd/wshcmd-rhyme.go
Normal file
@ -0,0 +1,320 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
|
||||
)
|
||||
|
||||
const OPENAI_API_KEY = ""
|
||||
|
||||
var rhymeCmd = &cobra.Command{
|
||||
Use: "rhyme [word]",
|
||||
Short: "find rhyming words using ChatGPT",
|
||||
RunE: rhymeRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(rhymeCmd)
|
||||
}
|
||||
|
||||
type RhymeCategory struct {
|
||||
Type string
|
||||
Words []string
|
||||
}
|
||||
|
||||
func RhymeStyleTag(ctx context.Context, props map[string]any) any {
|
||||
return vdom.Bind(`
|
||||
<style>
|
||||
.rhyme-container {
|
||||
padding: 20px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.word-header {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 40px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
background: #fdf0ed;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.category {
|
||||
margin-bottom: 30px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2980b9;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 2px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.word-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.word-item {
|
||||
background: #e8f0fe;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
border: 1px solid #d0e1fd;
|
||||
}
|
||||
|
||||
.word-item:hover {
|
||||
background: #d0e1fd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 40px;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
`, nil)
|
||||
}
|
||||
func RhymeContentTag(ctx context.Context, props map[string]any) any {
|
||||
word := props["word"].(string)
|
||||
isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool)
|
||||
errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string)
|
||||
rhymeData := GlobalVDomClient.GetAtomVal("rhymeData")
|
||||
|
||||
if isLoading {
|
||||
return vdom.Bind(`
|
||||
<div className="rhyme-container">
|
||||
<div className="word-header"><bindparam key="word"/></div>
|
||||
<div className="loading">Finding rhymes...</div>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"word": word,
|
||||
})
|
||||
}
|
||||
|
||||
if errorMsg != "" {
|
||||
return vdom.Bind(`
|
||||
<div className="rhyme-container">
|
||||
<div className="word-header"><bindparam key="word"/></div>
|
||||
<div className="error"><bindparam key="error"/></div>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"word": word,
|
||||
"error": errorMsg,
|
||||
})
|
||||
}
|
||||
|
||||
if rhymeData == nil {
|
||||
return vdom.Bind(`
|
||||
<div className="rhyme-container">
|
||||
<div className="word-header"><bindparam key="word"/></div>
|
||||
<div className="no-results">No rhymes found</div>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"word": word,
|
||||
})
|
||||
}
|
||||
|
||||
categories := rhymeData.([]RhymeCategory)
|
||||
if len(categories) == 0 {
|
||||
return vdom.Bind(`
|
||||
<div className="rhyme-container">
|
||||
<div className="word-header"><bindparam key="word"/></div>
|
||||
<div className="no-results">No rhymes found</div>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"word": word,
|
||||
})
|
||||
}
|
||||
|
||||
template := `
|
||||
<div className="rhyme-container">
|
||||
<div className="word-header">Rhymes for "<bindparam key="word"/>"</div>`
|
||||
|
||||
for _, category := range categories {
|
||||
template += fmt.Sprintf(`
|
||||
<div className="category">
|
||||
<div className="category-title">%s</div>
|
||||
<div className="word-grid">`, category.Type)
|
||||
|
||||
for _, rhyme := range category.Words {
|
||||
template += fmt.Sprintf(`
|
||||
<div className="word-item">%s</div>`, rhyme)
|
||||
}
|
||||
|
||||
template += `
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
template += `</div>`
|
||||
|
||||
return vdom.Bind(template, map[string]any{
|
||||
"word": word,
|
||||
})
|
||||
}
|
||||
|
||||
func findRhymes(client *openai.Client, word string) ([]RhymeCategory, error) {
|
||||
prompt := fmt.Sprintf(`For the word "%s", provide rhyming words in these categories:
|
||||
1. Perfect rhymes (same ending sound)
|
||||
2. Near rhymes (similar ending sound)
|
||||
3. Family rhymes (same word ending)
|
||||
|
||||
Format the response as a simple list with categories and words like this:
|
||||
Perfect rhymes: word1, word2, word3
|
||||
Near rhymes: word1, word2, word3
|
||||
Family rhymes: word1, word2, word3
|
||||
|
||||
Only include words that actually exist in English.`, word)
|
||||
|
||||
resp, err := client.CreateChatCompletion(
|
||||
context.Background(),
|
||||
openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: 0.7,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
response := resp.Choices[0].Message.Content
|
||||
categories := []RhymeCategory{}
|
||||
|
||||
// Split into lines and parse each category
|
||||
lines := strings.Split(response, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
categoryType := strings.TrimSpace(parts[0])
|
||||
wordList := strings.Split(strings.TrimSpace(parts[1]), ",")
|
||||
|
||||
// Clean up the words
|
||||
words := make([]string, 0)
|
||||
for _, word := range wordList {
|
||||
word = strings.TrimSpace(word)
|
||||
if word != "" {
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
|
||||
if len(words) > 0 {
|
||||
categories = append(categories, RhymeCategory{
|
||||
Type: categoryType,
|
||||
Words: words,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
func rhymeRun(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("please provide a word to find rhymes for")
|
||||
}
|
||||
word := args[0]
|
||||
|
||||
client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GlobalVDomClient = client
|
||||
|
||||
// Register components
|
||||
client.RegisterComponent("RhymeStyleTag", RhymeStyleTag)
|
||||
client.RegisterComponent("RhymeContentTag", RhymeContentTag)
|
||||
|
||||
// Initialize state
|
||||
client.SetAtomVal("isLoading", true)
|
||||
client.SetAtomVal("errorMsg", "")
|
||||
client.SetAtomVal("rhymeData", nil)
|
||||
|
||||
// Set root element
|
||||
client.SetRootElem(vdom.Bind(`
|
||||
<div>
|
||||
<RhymeStyleTag/>
|
||||
<RhymeContentTag word="#param:word"/>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"word": word,
|
||||
}))
|
||||
|
||||
// Create VDOM context
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start fetching rhymes
|
||||
go func() {
|
||||
openaiClient := openai.NewClient(OPENAI_API_KEY)
|
||||
rhymes, err := findRhymes(openaiClient, word)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error finding rhymes: %v", err)
|
||||
client.SetAtomVal("errorMsg", fmt.Sprintf("Error finding rhymes: %v", err))
|
||||
} else {
|
||||
client.SetAtomVal("rhymeData", rhymes)
|
||||
}
|
||||
|
||||
client.SetAtomVal("isLoading", false)
|
||||
client.SendAsyncInitiation()
|
||||
}()
|
||||
|
||||
<-client.DoneCh
|
||||
return nil
|
||||
}
|
255
cmd/wsh/cmd/wshcmd-vdom.go
Normal file
255
cmd/wsh/cmd/wshcmd-vdom.go
Normal file
@ -0,0 +1,255 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
|
||||
)
|
||||
|
||||
var moleCmd = &cobra.Command{
|
||||
Use: "mole",
|
||||
Hidden: true,
|
||||
Short: "launch whack-a-mole game",
|
||||
RunE: moleRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(moleCmd)
|
||||
}
|
||||
func MoleCellTag(ctx context.Context, props map[string]any) any {
|
||||
index := props["index"].(int)
|
||||
molePosition := GlobalVDomClient.GetAtomVal("molePosition").(int)
|
||||
|
||||
className := "cell"
|
||||
showMole := false
|
||||
if molePosition == index {
|
||||
className += " mole"
|
||||
showMole = true
|
||||
}
|
||||
|
||||
template := `
|
||||
<button className="#param:className" onClick="#param:clickHandler">`
|
||||
|
||||
if showMole {
|
||||
// This is our SVG converted to base64
|
||||
template += `<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSIzNSIgZmlsbD0iIzhCNDUxMyIvPjxlbGxpcHNlIGN4PSI1MCIgY3k9IjYwIiByeD0iMjUiIHJ5PSIyMCIgZmlsbD0iI0RFQjg4NyIvPjxlbGxpcHNlIGN4PSI1MCIgY3k9IjQ1IiByeD0iMTIiIHJ5PSI4IiBmaWxsPSIjNDYzMjIyIi8+PGNpcmNsZSBjeD0iMzUiIGN5PSIzNSIgcj0iNSIgZmlsbD0iYmxhY2siLz48Y2lyY2xlIGN4PSI2NSIgY3k9IjM1IiByPSI1IiBmaWxsPSJibGFjayIvPjxjaXJjbGUgY3g9IjMzIiBjeT0iMzMiIHI9IjIiIGZpbGw9IndoaXRlIi8+PGNpcmNsZSBjeD0iNjMiIGN5PSIzMyIgcj0iMiIgZmlsbD0id2hpdGUiLz48bGluZSB4MT0iMzUiIHkxPSI0NSIgeDI9IjIwIiB5Mj0iNDAiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIvPjxsaW5lIHgxPSIzNSIgeTE9IjQ4IiB4Mj0iMjAiIHkyPSI0OCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIi8+PGxpbmUgeDE9IjM1IiB5MT0iNTEiIHgyPSIyMCIgeTI9IjU2IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiLz48bGluZSB4MT0iNjUiIHkxPSI0NSIgeDI9IjgwIiB5Mj0iNDAiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIvPjxsaW5lIHgxPSI2NSIgeTE9IjQ4IiB4Mj0iODAiIHkyPSI0OCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIi8+PGxpbmUgeDE9IjY1IiB5MT0iNTEiIHgyPSI4MCIgeTI9IjU2IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=" width="60" height="60" alt="mole"></img>`
|
||||
}
|
||||
|
||||
template += `</button>`
|
||||
|
||||
return vdom.Bind(template, map[string]any{
|
||||
"className": className,
|
||||
"clickHandler": props["onCellClick"],
|
||||
})
|
||||
}
|
||||
|
||||
func MoleStyleTag(ctx context.Context, props map[string]any) any {
|
||||
return vdom.Bind(`
|
||||
<style>
|
||||
.game-container {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(3, 100px);
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cell.mole {
|
||||
background-color: #8a6343;
|
||||
}
|
||||
|
||||
.cell:hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.cell img {
|
||||
animation: pop-up 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pop-up {
|
||||
0% { transform: translateY(50px) scale(0.5); opacity: 0; }
|
||||
100% { transform: translateY(0) scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.start-button {
|
||||
margin-top: 20px;
|
||||
padding: 10px 20px;
|
||||
font-size: 18px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.start-button:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
</style>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
var moleScore int = 0
|
||||
|
||||
func MoleGameTag(ctx context.Context, props map[string]any) any {
|
||||
makeHandleCellClick := func(index int) func() {
|
||||
return func() {
|
||||
currentScore := GlobalVDomClient.GetAtomVal("moleScore").(int)
|
||||
molePosition := GlobalVDomClient.GetAtomVal("molePosition").(int)
|
||||
isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool)
|
||||
|
||||
log.Printf("cell clicked: %d (active:%v)\n", index, isActive)
|
||||
|
||||
if !isActive {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we clicked the right mole
|
||||
if molePosition == index {
|
||||
GlobalVDomClient.SetAtomVal("moleScore", currentScore+1)
|
||||
moleScore++
|
||||
|
||||
// Move mole to new random position
|
||||
newPosition := rand.Intn(9)
|
||||
GlobalVDomClient.SetAtomVal("molePosition", newPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleGame := func() {
|
||||
isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool)
|
||||
if isActive {
|
||||
GlobalVDomClient.SetAtomVal("moleGameActive", false)
|
||||
GlobalVDomClient.SetAtomVal("moleScore", 0)
|
||||
moleScore = 0
|
||||
GlobalVDomClient.SetAtomVal("molePosition", -1)
|
||||
} else {
|
||||
GlobalVDomClient.SetAtomVal("moleGameActive", true)
|
||||
GlobalVDomClient.SetAtomVal("molePosition", rand.Intn(9))
|
||||
}
|
||||
}
|
||||
|
||||
isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool)
|
||||
buttonText := "Start Game"
|
||||
if isActive {
|
||||
buttonText = "Stop Game"
|
||||
}
|
||||
|
||||
return vdom.Bind(`
|
||||
<div className="game-container">
|
||||
<MoleStyleTag/>
|
||||
<div className="score">Score: <bindparam key="moleScore"/></div>
|
||||
<div className="grid">
|
||||
<MoleCellTag index="#param:index0" onCellClick="#param:click0"/>
|
||||
<MoleCellTag index="#param:index1" onCellClick="#param:click1"/>
|
||||
<MoleCellTag index="#param:index2" onCellClick="#param:click2"/>
|
||||
<MoleCellTag index="#param:index3" onCellClick="#param:click3"/>
|
||||
<MoleCellTag index="#param:index4" onCellClick="#param:click4"/>
|
||||
<MoleCellTag index="#param:index5" onCellClick="#param:click5"/>
|
||||
<MoleCellTag index="#param:index6" onCellClick="#param:click6"/>
|
||||
<MoleCellTag index="#param:index7" onCellClick="#param:click7"/>
|
||||
<MoleCellTag index="#param:index8" onCellClick="#param:click8"/>
|
||||
</div>
|
||||
<button className="start-button" onClick="#param:toggleGame">
|
||||
<bindparam key="buttonText"/>
|
||||
</button>
|
||||
</div>
|
||||
`, map[string]any{
|
||||
"toggleGame": toggleGame,
|
||||
"buttonText": buttonText,
|
||||
"index0": 0,
|
||||
"index1": 1,
|
||||
"index2": 2,
|
||||
"index3": 3,
|
||||
"index4": 4,
|
||||
"index5": 5,
|
||||
"index6": 6,
|
||||
"index7": 7,
|
||||
"index8": 8,
|
||||
"click0": makeHandleCellClick(0),
|
||||
"click1": makeHandleCellClick(1),
|
||||
"click2": makeHandleCellClick(2),
|
||||
"click3": makeHandleCellClick(3),
|
||||
"click4": makeHandleCellClick(4),
|
||||
"click5": makeHandleCellClick(5),
|
||||
"click6": makeHandleCellClick(6),
|
||||
"click7": makeHandleCellClick(7),
|
||||
"click8": makeHandleCellClick(8),
|
||||
"moleScore": strconv.Itoa(moleScore),
|
||||
})
|
||||
}
|
||||
|
||||
func moleRun(cmd *cobra.Command, args []string) error {
|
||||
client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GlobalVDomClient = client
|
||||
|
||||
// Initialize random seed
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Register components
|
||||
client.RegisterComponent("MoleStyleTag", MoleStyleTag)
|
||||
client.RegisterComponent("MoleCellTag", MoleCellTag)
|
||||
client.RegisterComponent("MoleGameTag", MoleGameTag)
|
||||
|
||||
// Initialize state
|
||||
client.SetAtomVal("moleScore", 0)
|
||||
client.SetAtomVal("molePosition", -1)
|
||||
client.SetAtomVal("moleGameActive", false)
|
||||
|
||||
// Set root element
|
||||
client.SetRootElem(vdom.Bind(`<MoleGameTag/>`, nil))
|
||||
|
||||
// Create VDOM context
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start game loop when active
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
if client.GetAtomVal("moleGameActive").(bool) {
|
||||
newPos := rand.Intn(9)
|
||||
client.SetAtomVal("molePosition", newPos)
|
||||
log.Printf("new mole position: %d\n", newPos)
|
||||
client.SendAsyncInitiation()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-client.DoneCh
|
||||
return nil
|
||||
}
|
282
cmd/wsh/cmd/wshcmd-word.go
Normal file
282
cmd/wsh/cmd/wshcmd-word.go
Normal file
@ -0,0 +1,282 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
|
||||
)
|
||||
|
||||
// DictionaryResponse represents the API response structure
|
||||
type DictionaryResponse []struct {
|
||||
Word string `json:"word"`
|
||||
Phonetic string `json:"phonetic"`
|
||||
Meanings []Meaning `json:"meanings"`
|
||||
}
|
||||
|
||||
type Meaning struct {
|
||||
PartOfSpeech string `json:"partOfSpeech"`
|
||||
Definitions []struct {
|
||||
Definition string `json:"definition"`
|
||||
Example string `json:"example"`
|
||||
Synonyms []string `json:"synonyms"`
|
||||
} `json:"definitions"`
|
||||
}
|
||||
|
||||
var wordCmd = &cobra.Command{
|
||||
Use: "word",
|
||||
Hidden: true,
|
||||
Short: "show word of the day",
|
||||
RunE: wordRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(wordCmd)
|
||||
}
|
||||
|
||||
func WordStyleTag(ctx context.Context, props map[string]any) any {
|
||||
return vdom.Bind(`
|
||||
<style>
|
||||
.word-container {
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.word-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.phonetic {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.part-of-speech {
|
||||
font-size: 20px;
|
||||
color: #2980b9;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.definition {
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.example {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-top: 5px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.synonyms {
|
||||
margin-top: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.synonym-tag {
|
||||
display: inline-block;
|
||||
background: #f0f0f0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
`, nil)
|
||||
}
|
||||
func WordContentTag(ctx context.Context, props map[string]any) any {
|
||||
wordData := GlobalVDomClient.GetAtomVal("wordData")
|
||||
isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool)
|
||||
errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string)
|
||||
|
||||
if isLoading {
|
||||
return vdom.Bind(`
|
||||
<div className="loading">Loading word of the day...</div>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
if errorMsg != "" {
|
||||
return vdom.Bind(`
|
||||
<div className="error"><bindparam key="error"/></div>
|
||||
`, map[string]any{
|
||||
"error": errorMsg,
|
||||
})
|
||||
}
|
||||
|
||||
if wordData == nil {
|
||||
return vdom.Bind(`
|
||||
<div className="loading">Initializing...</div>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
response := wordData.(DictionaryResponse)
|
||||
if len(response) == 0 {
|
||||
return vdom.Bind(`
|
||||
<div className="error">No word data found</div>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
word := response[0]
|
||||
|
||||
template := `
|
||||
<div className="word-container">
|
||||
<div className="word-title"><bindparam key="word"/></div>
|
||||
<div className="phonetic"><bindparam key="phonetic"/></div>
|
||||
`
|
||||
|
||||
for _, meaning := range word.Meanings {
|
||||
template += fmt.Sprintf(`
|
||||
<div className="part-of-speech">%s</div>
|
||||
`, meaning.PartOfSpeech)
|
||||
|
||||
for _, def := range meaning.Definitions {
|
||||
template += fmt.Sprintf(`
|
||||
<div className="definition">%s</div>
|
||||
`, def.Definition)
|
||||
|
||||
if def.Example != "" {
|
||||
template += fmt.Sprintf(`
|
||||
<div className="example">"%s"</div>
|
||||
`, def.Example)
|
||||
}
|
||||
|
||||
if len(def.Synonyms) > 0 {
|
||||
template += `<div className="synonyms">`
|
||||
for _, syn := range def.Synonyms[:min(5, len(def.Synonyms))] {
|
||||
template += fmt.Sprintf(`
|
||||
<span className="synonym-tag">%s</span>
|
||||
`, syn)
|
||||
}
|
||||
template += `</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template += `</div>`
|
||||
|
||||
return vdom.Bind(template, map[string]any{
|
||||
"word": word.Word,
|
||||
"phonetic": word.Phonetic,
|
||||
})
|
||||
}
|
||||
|
||||
// min returns the minimum of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// getRandomWord returns a random word to look up
|
||||
func getRandomWord() string {
|
||||
words := []string{
|
||||
"serendipity", "ephemeral", "mellifluous", "ineffable", "surreptitious",
|
||||
"ethereal", "melancholy", "perspicacious", "sanguine", "vociferous",
|
||||
"arduous", "mundane", "pragmatic", "resilient", "tenacious",
|
||||
}
|
||||
return words[time.Now().Unix()%int64(len(words))]
|
||||
}
|
||||
|
||||
func fetchWordDefinition(word string) (DictionaryResponse, error) {
|
||||
url := fmt.Sprintf("https://api.dictionaryapi.dev/api/v2/entries/en/%s", word)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result DictionaryResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func wordRun(cmd *cobra.Command, args []string) error {
|
||||
client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GlobalVDomClient = client
|
||||
|
||||
// Register components
|
||||
client.RegisterComponent("WordStyleTag", WordStyleTag)
|
||||
client.RegisterComponent("WordContentTag", WordContentTag)
|
||||
|
||||
// Initialize state
|
||||
client.SetAtomVal("wordData", nil)
|
||||
client.SetAtomVal("isLoading", true)
|
||||
client.SetAtomVal("errorMsg", "")
|
||||
|
||||
// Set root element
|
||||
client.SetRootElem(vdom.Bind(`
|
||||
<div>
|
||||
<WordStyleTag/>
|
||||
<WordContentTag/>
|
||||
</div>
|
||||
`, nil))
|
||||
|
||||
// Create VDOM context
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch word data
|
||||
go func() {
|
||||
word := getRandomWord()
|
||||
data, err := fetchWordDefinition(word)
|
||||
if err != nil {
|
||||
client.SetAtomVal("errorMsg", fmt.Sprintf("Error fetching word: %v", err))
|
||||
} else {
|
||||
client.SetAtomVal("wordData", data)
|
||||
}
|
||||
client.SetAtomVal("isLoading", false)
|
||||
client.SendAsyncInitiation()
|
||||
}()
|
||||
|
||||
<-client.DoneCh
|
||||
return nil
|
||||
}
|
@ -294,6 +294,9 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
return childrenComps;
|
||||
}
|
||||
props.key = "e-" + elem.waveid;
|
||||
if (elem.tag == "img") {
|
||||
childrenComps = null;
|
||||
}
|
||||
return React.createElement(elem.tag, props, childrenComps);
|
||||
}
|
||||
|
||||
|
1
go.mod
1
go.mod
@ -16,6 +16,7 @@ require (
|
||||
github.com/kevinburke/ssh_config v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/sashabaranov/go-openai v1.32.3
|
||||
github.com/sawka/txwrap v0.2.0
|
||||
github.com/shirou/gopsutil/v4 v4.24.9
|
||||
|
2
go.sum
2
go.sum
@ -51,6 +51,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg=
|
||||
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@ -65,7 +65,7 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
|
||||
impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
|
||||
}
|
||||
}
|
||||
if feUpdate.Resync {
|
||||
if feUpdate.Resync || true {
|
||||
return impl.Client.fullRender()
|
||||
}
|
||||
return impl.Client.incrementalRender()
|
||||
|
Loading…
Reference in New Issue
Block a user