mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +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;
|
return childrenComps;
|
||||||
}
|
}
|
||||||
props.key = "e-" + elem.waveid;
|
props.key = "e-" + elem.waveid;
|
||||||
|
if (elem.tag == "img") {
|
||||||
|
childrenComps = null;
|
||||||
|
}
|
||||||
return React.createElement(elem.tag, props, childrenComps);
|
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/kevinburke/ssh_config v1.2.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
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/sashabaranov/go-openai v1.32.3
|
||||||
github.com/sawka/txwrap v0.2.0
|
github.com/sawka/txwrap v0.2.0
|
||||||
github.com/shirou/gopsutil/v4 v4.24.9
|
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/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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
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 h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg=
|
||||||
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk=
|
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=
|
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)
|
impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if feUpdate.Resync {
|
if feUpdate.Resync || true {
|
||||||
return impl.Client.fullRender()
|
return impl.Client.fullRender()
|
||||||
}
|
}
|
||||||
return impl.Client.incrementalRender()
|
return impl.Client.incrementalRender()
|
||||||
|
Loading…
Reference in New Issue
Block a user