feat: home page
All checks were successful
Build / Build (push) Successful in 1m22s

This commit is contained in:
洛天依 2025-01-11 04:31:04 +00:00
parent f06f341d12
commit 644e1339fb
Signed by: luo
SSH Key Fingerprint: SHA256:V1KdsvGUpiKVfrJo1oHrAPnc/Z6k/6xgaZN7iTbYBl4
20 changed files with 402 additions and 251 deletions

21
.editorconfig Normal file
View File

@ -0,0 +1,21 @@
; https://editorconfig.org/
root = true
[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4
[{*.yml,*.yaml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -1,13 +1,6 @@
on: [push, pull_request, workflow_dispatch]
name: Build
on:
push:
branches:
- main
tags:
- v*
workflow_dispatch:
jobs:
build:
name: Build
@ -17,6 +10,9 @@ jobs:
contents: write
pull-requests: read
checks: write
env:
DOCKER_REGISTRY: devops.lty.name
DOCKER_IMAGE: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
@ -26,36 +22,33 @@ jobs:
go-version: '1.23'
- uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8
with:
version: v1.60
- name: Build
run: make -j8 all && tree bin
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: builds-${{ github.sha }}
path: |
bin/*.zip
bin/*.tar.gz
bin/*.sha256sum
- name: Upload Release Asset
if: startsWith(github.ref, 'refs/tags/')
version: v1.63
- name: build
run: |
gh release create ${GIT_TAG} bin/*.zip bin/*.tar.gz bin/*.sha256sum
make build
cp -r bin ${DISTNAME}
cp -r agents ${DISTNAME}
tar --zstd -cvf ${DISTNAME}.tar.zst ${DISTNAME}
env:
GIT_TAG: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DISTNAME: chatai-linux-amd64-${{ github.ref_name }}
- if: startsWith(github.ref, 'refs/tags/')
uses: https://gitea.com/actions/release-action@21a5938ff2548f6472d89ba13b3cdd8af4c67068
with:
api_key: ${{ secrets.GITHUB_TOKEN }}
files: |
*.tar.zst
- uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.PACKAGES_PUBLISH_TOKEN }}
- if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/386
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ startsWith(github.ref, 'refs/tags/') && 'latest' || 'dev' }}
ghcr.io/${{ github.repository }}:${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.sha }}
${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ startsWith(github.ref, 'refs/tags/') && 'latest' || 'dev' }}
${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.sha }}

View File

@ -1,13 +1,7 @@
FROM --platform=$BUILDPLATFORM alpine:3.21 AS builder
RUN apk add --no-cache tzdata ca-certificates
FROM alpine:3.21
ARG TARGETPLATFORM
COPY bin/arch/${TARGETPLATFORM}/ai-agent /app/ai-agent
COPY agents /app/agents
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY ./bin/chatai /app/chatai
COPY ./agents /app/agents
RUN apk add --no-cache tzdata ca-certificates
WORKDIR /app
EXPOSE 7120
CMD ["/app/ai-agent"]
CMD ["/app/chatai"]

View File

@ -1,30 +1,10 @@
#!/usr/bin/env make -f
AGENTS_DIR = agents
DIST_DIRECTORY = ./bin
DIST_APPNAME = ai-agent
DIST_EXENAME = ai-agent
BUILDINFO_CLASS = github.com/ltylab/ai-agent/internal/conf
BUILDINFO_VERSION := $(shell git describe --tags 2>/dev/null || echo "git/$(shell git rev-parse --short HEAD 2>/dev/null || echo 'none')")
CROSS_BUILD_TRIPLES = darwin/amd64 \
darwin/arm64 \
windows/386 \
windows/amd64 \
windows/arm64 \
linux/386 \
linux/amd64 \
linux/arm64 \
linux/loong64 \
linux/mips \
linux/mips64 \
linux/mips64le \
linux/mipsle \
linux/riscv64 \
freebsd/386 \
freebsd/amd64 \
freebsd/arm64 \
freebsd/riscv64
OUTPUT := ./bin/
VERSION := $(shell git describe --tags 2>/dev/null || echo "git/$(shell git rev-parse --short HEAD 2>/dev/null || echo 'none')")
CGO_ENABLED := 0
BUILDINFO_CLASS := devops.lty.name/luo/chatai/internal/config
LDFLAGS = -s -w
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildVersion=$(BUILDINFO_VERSION)"
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildVersion=$(VERSION)"
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildTime=$(shell date '+%Y-%m-%d %H:%M:%S %Z')"
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildMode=release"
@ -36,7 +16,12 @@ build_debug: build
.PHONY: build
build: clean install
CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o $(DIST_DIRECTORY)/$(DIST_EXENAME)
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags '$(LDFLAGS)' -o $(OUTPUT)
.PHONY: run
run: install
go run .
.PHONY: install
@ -46,26 +31,4 @@ install:
.PHONY: clean
clean:
rm -rvf $(DIST_DIRECTORY)
.PHONY: all $(CROSS_BUILD_TRIPLES)
all: clean install $(CROSS_BUILD_TRIPLES)
$(CROSS_BUILD_TRIPLES): GOOS = $(word 1,$(subst /, ,$@))
$(CROSS_BUILD_TRIPLES): GOARCH = $(word 2,$(subst /, ,$@))
$(CROSS_BUILD_TRIPLES): CROSS_DIST_DIRNAME = $(DIST_APPNAME)-$(subst /,.,$(BUILDINFO_VERSION)-$(GOOS)-$(GOARCH))
$(CROSS_BUILD_TRIPLES): CROSS_DIST_EXENAME = $(DIST_EXENAME)$(if $(filter $(GOOS),windows),.exe,)
$(CROSS_BUILD_TRIPLES): CROSS_DIST_ARCNAME = $(CROSS_DIST_DIRNAME).$(if $(filter $(GOOS),windows),zip,tar.gz)
$(CROSS_BUILD_TRIPLES):
$(MAKE) build GOOS=$(GOOS) GOARCH=$(GOARCH) \
DIST_DIRECTORY=$(DIST_DIRECTORY)/$(CROSS_DIST_DIRNAME) \
DIST_EXENAME=$(CROSS_DIST_EXENAME) && \
cp -r $(AGENTS_DIR) $(DIST_DIRECTORY)/$(CROSS_DIST_DIRNAME)/ && \
mkdir -p $(DIST_DIRECTORY)/arch/$(GOOS)/$(GOARCH) && \
cp $(DIST_DIRECTORY)/$(CROSS_DIST_DIRNAME)/$(CROSS_DIST_EXENAME) $(DIST_DIRECTORY)/arch/$(GOOS)/$(GOARCH)/$(CROSS_DIST_EXENAME) && \
if [ "$(GOOS)" = "windows" ]; then \
cd $(DIST_DIRECTORY) && zip -r $(CROSS_DIST_ARCNAME) $(CROSS_DIST_DIRNAME) && cd -; \
else \
tar -cvzf $(DIST_DIRECTORY)/$(CROSS_DIST_ARCNAME) -C $(DIST_DIRECTORY) $(CROSS_DIST_DIRNAME); \
fi && \
sh -c "cd $(DIST_DIRECTORY) && sha256sum $(CROSS_DIST_ARCNAME) > $(CROSS_DIST_ARCNAME).sha256sum";
rm -rvf $(OUTPUT)

View File

@ -1,35 +1,34 @@
# Luo Tianyi Codelabs AI Agent
# Chat AI
提供类似 QQ 界面的公共 AI 聊天机器人,基于 Google Gemini 模型。
**注意**
- 不支持视频、图片、音频等多模态输入。
- 完全公开、无频率限制与身份认证。建议使用免费的 API Key 并禁用结算账号。
- 暂时没有列出所有模型的功能。
## 使用方法
GitHub Release 下载最新版本的压缩包,解压缩。你可以使用 `-h` 参数查看帮助信息:
「发行版」下载最新版本的压缩包,解压缩。你可以使用 `-h` 参数查看帮助信息:
```bash
./ai-agent-web -h
./chatai -h
```
要正常启动服务器,您需要设置环境变量 `GOOGLE_AI_KEY`。您可以在 [Google AI Studio](https://aistudio.google.com) 免费获得一个(需要 Google 账号)。
```bash
export GOOGLE_AI_KEY=your-key
./ai-agent-web
./chatai
```
您也可以使用 Docker 运行:
```bash
docker run -d -p 57120:7120 -e GOOGLE_AI_KEY=your-key \
--name ai-agent-web ghcr.io/github.com/ltylab/ai-agent
--name chatai lty.name/chatai
```
如果需要自定义 AI Agent请将您的 AI Agent 配置文件映射到 `/app/agents` 目录:
```bash
docker run -d -p 57120:7120 -e GOOGLE_AI_KEY=your-key \
-v /path/to/your/agents:/app/agents \
--name ai-agent-web ghcr.io/github.com/ltylab/ai-agent
--name chatai lty.name/chatai
```
可以通过 `-model` 参数修改模型,推荐使用下面两个免费模型:
@ -55,6 +54,7 @@ agents
"id": "luo",
"name": "洛天依AI",
"desc": "世界第一的 ∞ 吃货殿下~",
"footer": "此 AI 并非洛天依官方提供,请勿用于商业用途。",
"primaryColor": "#6cb6df",
"secondaryColor": "#66ccff",
"accentColor": "#a62f35"
@ -65,6 +65,7 @@ agents
- `id` 是 Agent ID必须和文件夹名称一致。
- `name` 是 Agent 名称,将显示在聊天界面。
- `desc` 是 Agent 描述,将显示在聊天界面。
- `footer` 是 Agent 页脚,用于显示版权信息、免责声明等。
- `primaryColor` 是主色调,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
- `secondaryColor` 是次色调,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
- `accentColor` 是强调色,将用于聊天界面的背景色。可选,如果未设置则使用默认值。

View File

@ -1,5 +1,6 @@
{
"id": "ldk",
"name": "李迪克AI",
"desc": "我跟你讲!我家星尘宝宝,可爱(*´▽`*)"
"desc": "我跟你讲!我家星尘宝宝,可爱(*´▽`*)",
"footer": "AI生成的内容不代表 <a href=\"https://space.bilibili.com/882467\" target=\"_blank\" rel=\"noopener noreferrer\">李迪克</a> 的观点 | 数据来源: <a href=\"https://cyberldk.com\" target=\"_blank\" rel=\"noopener noreferrer\">CyberLDK</a>"
}

View File

@ -2,6 +2,7 @@
"id": "luo",
"name": "洛天依AI",
"desc": "世界第一的 ∞ 吃货殿下~",
"footer": "此 AI 并非官方制作 | 请勿用于不当或商业用途",
"primaryColor": "#6cb6df",
"secondaryColor": "#66ccff",
"accentColor": "#a62f35"

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/ltylab/ai-agent
module devops.lty.name/luo/chatai
go 1.23.3

View File

@ -7,9 +7,9 @@ import (
"path"
"regexp"
"github.com/ltylab/ai-agent/internal/conf"
"github.com/ltylab/ai-agent/internal/log"
"github.com/ltylab/ai-agent/web/assets"
"devops.lty.name/luo/chatai/internal/config"
"devops.lty.name/luo/chatai/internal/log"
"devops.lty.name/luo/chatai/web/assets"
"github.com/google/uuid"
)
@ -24,6 +24,9 @@ type AgentConf struct {
// AgentDescription is a short description of the agent.
AgentDescription string `json:"desc"`
// FooterHTML is the HTML content to be displayed in the footer of the agent chat page.
FooterHTML string `json:"footer"`
// PrimaryColor is the primary color for the agent chat page.
PrimaryColor string `json:"primaryColor"`
@ -78,11 +81,11 @@ func (a *AgentConf) ReadAgent() *Agent {
func LoadAgentFromFile(agentJSONPath string) (*AgentConf, error) {
conf := &AgentConf{
AgentID: uuid.New().String(),
AgentName: conf.DefaultAgentName,
AgentDescription: conf.DefaultAgentDescription,
PrimaryColor: conf.DefaultAgentPrimaryColor,
SecondaryColor: conf.DefaultAgentSecondaryColor,
AccentColor: conf.DefaultAgentAccentColor,
AgentName: config.DefaultAgentName,
AgentDescription: config.DefaultAgentDescription,
PrimaryColor: config.DefaultAgentPrimaryColor,
SecondaryColor: config.DefaultAgentSecondaryColor,
AccentColor: config.DefaultAgentAccentColor,
Dir: path.Dir(agentJSONPath),
}

View File

@ -1,22 +0,0 @@
package conf
var (
BuildVersion = "dev"
BuildTime = "<unknown>"
BuildMode = "development"
PackageLicense = "Unlicense"
PackageCopyright = "This is an unlicensed software (under public domain) by Luo Tianyi Codelabs <https://lty.name/>."
DefaultListen = ":7120"
DefaultAgentsDir = "agents"
DefaultLogLevel = "dbg"
DefaultMsgLogPath = "logs"
DefaultAgentName = "未名"
DefaultAgentDescription = "这个 Agent 没有名字"
DefaultAgentPrimaryColor = "#444e8d"
DefaultAgentSecondaryColor = "#9f9ff5"
DefaultAgentAccentColor = "#eeaf5b"
DefaultGoogleAIModel = "gemini-1.5-flash"
)

21
internal/config/conf.go Normal file
View File

@ -0,0 +1,21 @@
package config
var (
BuildVersion = "dev"
BuildTime = "<unknown>"
BuildMode = "development"
Copyright = "This is an unlicensed software (under public domain) by Luo Tianyi Codelabs <https://lty.name/>."
FlagListen = ":7120"
FlagAgentsDir = "agents"
FlagLogLevel = "dbg"
FlagMsgLogPath = "logs"
DefaultAgentName = "未名"
DefaultAgentDescription = "这个 Agent 没有名字"
DefaultAgentPrimaryColor = "#444e8d"
DefaultAgentSecondaryColor = "#9f9ff5"
DefaultAgentAccentColor = "#eeaf5b"
DefaultGoogleAIModel = "gemini-1.5-flash"
)

View File

@ -8,8 +8,8 @@ import (
"path"
"time"
"github.com/ltylab/ai-agent/internal/agent"
"github.com/ltylab/ai-agent/internal/log"
"devops.lty.name/luo/chatai/internal/agent"
"devops.lty.name/luo/chatai/internal/log"
"github.com/gin-gonic/gin"
"github.com/google/generative-ai-go/genai"

View File

@ -1,15 +1,15 @@
package server
import (
"html/template"
"text/template"
"net/http"
"os"
"github.com/ltylab/ai-agent/internal/agent"
"github.com/ltylab/ai-agent/internal/log"
"github.com/ltylab/ai-agent/web"
"github.com/ltylab/ai-agent/web/assets"
"devops.lty.name/luo/chatai/internal/agent"
"devops.lty.name/luo/chatai/internal/log"
"devops.lty.name/luo/chatai/web"
"devops.lty.name/luo/chatai/web/assets"
"github.com/gin-gonic/gin"
)
@ -38,11 +38,32 @@ func Setup(e *gin.Engine, agentsDir string, msgLogPath string) {
}
e.StaticFS("/assets", http.FS(assets.AssetsFS))
e.GET("", s.handleHomepageWithRendenedTemplate)
e.GET("/:agentID", s.handleWithRendenedTemplate)
e.GET("/:agentID/avatar.webp", s.handleAvatar)
e.POST("/:agentID/chat", s.handleChat)
}
func (s *Server) handleHomepageWithRendenedTemplate(c *gin.Context) {
tmpl, err := template.New("home").Parse(web.HomeLayout)
if err != nil {
log.T("server/tmpl").Errf("Failed to parse template: %v", err)
c.String(http.StatusInternalServerError, "server error")
return
}
err = tmpl.Execute(c.Writer, s.agents)
if err != nil {
log.T("server/tmpl").Errf("Failed to render template: %v", err)
c.String(http.StatusInternalServerError, "server error")
return
}
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
c.Writer.Flush()
c.Abort()
}
func (s *Server) handleWithRendenedTemplate(c *gin.Context) {
agentID := c.Param("agentID")
agentConf, ok := s.agents[agentID]
@ -51,7 +72,7 @@ func (s *Server) handleWithRendenedTemplate(c *gin.Context) {
return
}
tmpl, err := template.New("tmpl.html").Parse(web.Template)
tmpl, err := template.New("agent").Parse(web.AgentLayout)
if err != nil {
log.T("server/tmpl").Errf("Failed to parse template: %v", err)
c.String(http.StatusInternalServerError, "server error")

22
main.go
View File

@ -4,19 +4,19 @@ import (
"flag"
"os"
"github.com/ltylab/ai-agent/internal/conf"
"github.com/ltylab/ai-agent/internal/log"
"github.com/ltylab/ai-agent/internal/server"
"devops.lty.name/luo/chatai/internal/config"
"devops.lty.name/luo/chatai/internal/log"
"devops.lty.name/luo/chatai/internal/server"
"github.com/gin-gonic/gin"
)
func main() {
listen := flag.String("listen", conf.DefaultListen, "http listen address")
agentsDir := flag.String("agents", conf.DefaultAgentsDir, "directory to store agent configurations")
logLevel := flag.String("level", conf.DefaultLogLevel, "log level (dbg, inf, wrn, err)")
msgLogPath := flag.String("msglog", conf.DefaultMsgLogPath, "audit log path")
model := flag.String("model", conf.DefaultGoogleAIModel, "Google AI model (gemini-1.5-flash, gemini-2.0-flash-exp)")
listen := flag.String("listen", config.FlagListen, "http listen address")
agentsDir := flag.String("agents", config.FlagAgentsDir, "directory to store agent configurations")
logLevel := flag.String("level", config.FlagLogLevel, "log level (dbg, inf, wrn, err)")
msgLogPath := flag.String("msglog", config.FlagMsgLogPath, "audit log path")
model := flag.String("model", config.DefaultGoogleAIModel, "Google AI model (gemini-1.5-flash, gemini-2.0-flash-exp)")
flag.Parse()
server.GoogleAIKey = os.Getenv("GOOGLE_AI_KEY")
@ -27,11 +27,11 @@ func main() {
server.GoogleAIModel = *model
log.Setup(log.LogLevel(log.ParseLogLevel(*logLevel)))
log.T("main").Inff("AI-Agent version %s, %s mode (build %s)", conf.BuildVersion, conf.BuildMode, conf.BuildTime)
log.T("main").Inff("%s", conf.PackageCopyright)
log.T("main").Inff("AI-Agent version %s, %s mode (build %s)", config.BuildVersion, config.BuildMode, config.BuildTime)
log.T("main").Inff("%s", config.Copyright)
log.SetupGin1()
if conf.BuildMode == "release" {
if config.BuildMode == "release" {
gin.SetMode(gin.ReleaseMode)
}

View File

@ -248,7 +248,7 @@
<aside>
<button>_</button>
<button></button>
<button>×</button>
<button onclick="location.href = '/'">×</button>
</aside>
</header>
<article>
@ -264,7 +264,8 @@
</footer>
</main>
<div class="copy">
以上内容为 AI 生成,请注意辨别。<br>为了安全和审计需要,我们可能记录您与 AI 的对话内容和您的 IP 地址。请勿向模型分享敏感信息。
{{.FooterHTML}}<br>
以上内容为 AI 生成,请注意辨别。<br>为了安全和审计需要,我们可能记录您与 AI 的对话内容和您的 IP 地址。请勿向 AI 分享敏感信息。
</div>
<template id="chat-record">
<section>

View File

@ -4,5 +4,8 @@ import (
_ "embed"
)
//go:embed tmpl.html
var Template string
//go:embed home.html
var HomeLayout string
//go:embed agent.html
var AgentLayout string

150
web/home.html Normal file
View File

@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="renderer" content="webkit">
<meta http-equiv="Cache-Control" content="no-siteapp">
<meta name="google" content="notranslate">
<link rel="icon" type="image/png" href="/assets/logo.png">
<title>选择一个 AI Agent 开始聊天</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans',
'PingFang SC', 'Hiragino Sans GB', 'Source Han Sans CN', 'Source Han Sans SC', 'Microsoft YaHei', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei',
'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
}
h1 {
margin: 0;
margin-bottom: 3rem;
font-size: 1.8rem;
color: #3c3c3c;
text-align: center;
font-weight: 400;
}
main {
width: 100%;
height: 40%;
min-height: 400px;
flex-wrap: nowrap;
padding: 0 2rem;
}
main .container {
height: 100%;
padding: 1rem;
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
gap: 2.5rem;
justify-content: space-evenly;
align-items: center;
-ms-overflow-style: none;
scrollbar-width: none;
}
main .container::-webkit-scrollbar {
display: none;
}
section {
height: 100%;
background: #fcfcfc;
min-width: 300px;
max-width: 300px;
padding: 1rem;
border-radius: 1rem;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
text-align: center;
}
section img {
margin-top: 2rem;
margin-bottom: 1.5rem;
width: 3.5rem;
height: 3.5rem;
border-radius: .5rem;
}
section h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 400;
margin-bottom: 1.5rem;
}
section span {
font-size: 0.8rem;
color: #666;
margin-bottom: 1.5rem;
}
section a {
display: block;
width: 100%;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
text-decoration: none;
transition: all 0.3s;
justify-self: flex-end;
cursor: pointer;
margin-bottom: .5rem;
transition: all 0.3s ease;
}
{{range .}}
.chat-btn__{{.AgentID}} {
background: {{.PrimaryColor}};
color: white;
}
.chat-btn__{{.AgentID}}:hover {
background: {{.SecondaryColor}};
}
{{end}}
</style>
</head>
<body>
<h1>选择一个 AI Agent 开始聊天</h1>
<main>
<div class="container">
{{range .}}
<section>
<div>
<img src="/{{.AgentID}}/avatar.webp" alt="{{.AgentName}} 头像">
<h2>{{.AgentName}}</h2>
<span>{{.AgentDescription}}</span>
</div>
<a href="/{{.AgentID}}" class="chat-btn__{{.AgentID}}">开始聊天</a>
</section>
{{end}}
</div>
</main>
<script>
window.addEventListener("wheel", function (e) {
const container = document.querySelector(".container");
if (e.deltaY > 0) container.scrollLeft += 100;
else container.scrollLeft -= 100;
});
</script>
</body>
</html>