Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
W
WebAgent
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
WebAgent
Commits
f86dd1a6
Commit
f86dd1a6
authored
Jul 15, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add authenticated workspace drafts
parent
10c7988e
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
280 additions
and
103 deletions
+280
-103
.env.example
.env.example
+2
-0
build-manager.ts
backend/src/build/build-manager.ts
+1
-1
config.ts
backend/src/config.ts
+4
-0
git-manager.ts
backend/src/git/git-manager.ts
+25
-0
schemas.ts
backend/src/schemas.ts
+5
-0
server.ts
backend/src/server.ts
+27
-2
site-agent-service.ts
backend/src/sites/site-agent-service.ts
+16
-38
site-repository.ts
backend/src/sites/site-repository.ts
+4
-0
site-version-service.ts
backend/src/sites/site-version-service.ts
+32
-0
storage-convention.md
docs/storage-convention.md
+4
-3
App.tsx
frontend/src/App.tsx
+138
-51
api.ts
frontend/src/api.ts
+6
-0
styles.css
frontend/src/styles.css
+10
-6
index.ts
shared/src/index.ts
+6
-2
No files found.
.env.example
View file @
f86dd1a6
PORT=3100
PORT=3100
HOST=127.0.0.1
HOST=127.0.0.1
FRONTEND_ORIGIN=http://localhost
FRONTEND_ORIGIN=http://localhost
# 单用户登录账号固定为 user;生产环境请务必修改默认密码。
WEBAGENT_USER_PASSWORD=user
# 相对路径以 WebAgent 项目根目录为基准,也支持 /data/WebAgent-sites 等绝对路径。
# 相对路径以 WebAgent 项目根目录为基准,也支持 /data/WebAgent-sites 等绝对路径。
WEBAGENT_SITES_DIR=../WebAgent-sites
WEBAGENT_SITES_DIR=../WebAgent-sites
# 已上线的静态产物独立保存,不会被测试环境构建覆盖。
# 已上线的静态产物独立保存,不会被测试环境构建覆盖。
...
...
backend/src/build/build-manager.ts
View file @
f86dd1a6
...
@@ -20,7 +20,7 @@ export class BuildManager {
...
@@ -20,7 +20,7 @@ export class BuildManager {
output
+=
install
.
stdout
+
"
\n
"
+
install
.
stderr
+
"
\n
"
;
output
+=
install
.
stdout
+
"
\n
"
+
install
.
stderr
+
"
\n
"
;
const
build
=
await
execa
(
"pnpm"
,
[
"run"
,
"build"
],
{
const
build
=
await
execa
(
"pnpm"
,
[
"run"
,
"build"
],
{
cwd
:
projectPath
,
cwd
:
projectPath
,
env
:
{
...
process
.
env
,
SITE_BASE_PATH
:
basePath
.
replace
(
/
\/
$/
,
""
)
||
"/"
},
env
:
{
...
process
.
env
,
CI
:
"true"
,
SITE_BASE_PATH
:
basePath
.
replace
(
/
\/
$/
,
""
)
||
"/"
},
});
});
output
+=
build
.
stdout
+
"
\n
"
+
build
.
stderr
;
output
+=
build
.
stdout
+
"
\n
"
+
build
.
stderr
;
await
writeFile
(
path
.
join
(
runtimePaths
.
logs
,
taskId
+
".log"
),
output
,
"utf8"
);
await
writeFile
(
path
.
join
(
runtimePaths
.
logs
,
taskId
+
".log"
),
output
,
"utf8"
);
...
...
backend/src/config.ts
View file @
f86dd1a6
...
@@ -22,6 +22,10 @@ export const config = {
...
@@ -22,6 +22,10 @@ export const config = {
host
:
process
.
env
.
HOST
||
"127.0.0.1"
,
host
:
process
.
env
.
HOST
||
"127.0.0.1"
,
port
:
Number
(
process
.
env
.
PORT
||
3100
),
port
:
Number
(
process
.
env
.
PORT
||
3100
),
frontendOrigin
:
process
.
env
.
FRONTEND_ORIGIN
||
"http://localhost:5173"
,
frontendOrigin
:
process
.
env
.
FRONTEND_ORIGIN
||
"http://localhost:5173"
,
auth
:
{
username
:
"user"
,
password
:
process
.
env
.
WEBAGENT_USER_PASSWORD
||
"user"
,
},
openai
:
{
openai
:
{
apiKey
:
process
.
env
.
OPENAI_API_KEY
||
""
,
apiKey
:
process
.
env
.
OPENAI_API_KEY
||
""
,
baseUrl
:
(
process
.
env
.
OPENAI_BASE_URL
||
"https://api.deepseek.com/v1"
).
replace
(
/
\/
$/
,
""
),
baseUrl
:
(
process
.
env
.
OPENAI_BASE_URL
||
"https://api.deepseek.com/v1"
).
replace
(
/
\/
$/
,
""
),
...
...
backend/src/git/git-manager.ts
View file @
f86dd1a6
...
@@ -45,6 +45,23 @@ export class GitManager {
...
@@ -45,6 +45,23 @@ export class GitManager {
await
execa
(
"git"
,
[
"worktree"
,
"add"
,
"--detach"
,
workspacePath
,
reference
],
{
cwd
:
projectPath
});
await
execa
(
"git"
,
[
"worktree"
,
"add"
,
"--detach"
,
workspacePath
,
reference
],
{
cwd
:
projectPath
});
}
}
async
createPersistentWorktree
(
projectPath
:
string
,
workspacePath
:
string
,
reference
=
"HEAD"
):
Promise
<
void
>
{
await
this
.
removePersistentWorktree
(
projectPath
,
workspacePath
);
await
mkdir
(
path
.
dirname
(
workspacePath
),
{
recursive
:
true
});
await
execa
(
"git"
,
[
"worktree"
,
"add"
,
"--detach"
,
workspacePath
,
reference
],
{
cwd
:
projectPath
});
}
async
removePersistentWorktree
(
projectPath
:
string
,
workspacePath
:
string
):
Promise
<
void
>
{
await
execa
(
"git"
,
[
"worktree"
,
"remove"
,
"--force"
,
workspacePath
],
{
cwd
:
projectPath
,
reject
:
false
});
await
execa
(
"git"
,
[
"worktree"
,
"prune"
],
{
cwd
:
projectPath
,
reject
:
false
});
await
rm
(
workspacePath
,
{
recursive
:
true
,
force
:
true
});
}
async
hasChanges
(
workspacePath
:
string
):
Promise
<
boolean
>
{
const
result
=
await
execa
(
"git"
,
[
"status"
,
"--porcelain"
],
{
cwd
:
workspacePath
});
return
result
.
stdout
.
trim
().
length
>
0
;
}
async
removeWorktree
(
projectPath
:
string
,
workspacePath
:
string
):
Promise
<
void
>
{
async
removeWorktree
(
projectPath
:
string
,
workspacePath
:
string
):
Promise
<
void
>
{
await
execa
(
"git"
,
[
"worktree"
,
"remove"
,
"--force"
,
workspacePath
],
{
cwd
:
projectPath
,
reject
:
false
});
await
execa
(
"git"
,
[
"worktree"
,
"remove"
,
"--force"
,
workspacePath
],
{
cwd
:
projectPath
,
reject
:
false
});
await
execa
(
"git"
,
[
"worktree"
,
"prune"
],
{
cwd
:
projectPath
,
reject
:
false
});
await
execa
(
"git"
,
[
"worktree"
,
"prune"
],
{
cwd
:
projectPath
,
reject
:
false
});
...
@@ -69,6 +86,14 @@ export class GitManager {
...
@@ -69,6 +86,14 @@ export class GitManager {
return
this
.
currentCommit
(
projectPath
);
return
this
.
currentCommit
(
projectPath
);
}
}
async
fastForward
(
projectPath
:
string
,
commit
:
string
,
expectedBase
:
string
):
Promise
<
string
>
{
const
current
=
await
this
.
currentCommit
(
projectPath
);
if
(
current
===
commit
)
return
current
;
if
(
current
!==
expectedBase
)
throw
new
Error
(
"最近保存版本已变化,请刷新后重新预览草稿"
);
await
execa
(
"git"
,
[
"merge"
,
"--ff-only"
,
commit
],
{
cwd
:
projectPath
});
return
this
.
currentCommit
(
projectPath
);
}
async
restoreAsCommit
(
projectPath
:
string
,
sourceCommit
:
string
):
Promise
<
string
>
{
async
restoreAsCommit
(
projectPath
:
string
,
sourceCommit
:
string
):
Promise
<
string
>
{
await
this
.
assertCommit
(
projectPath
,
sourceCommit
);
await
this
.
assertCommit
(
projectPath
,
sourceCommit
);
await
execa
(
"git"
,
[
"restore"
,
"--source"
,
sourceCommit
,
"--staged"
,
"--worktree"
,
"--"
,
"."
,
":(exclude)astro.config.mjs"
],
{
cwd
:
projectPath
});
await
execa
(
"git"
,
[
"restore"
,
"--source"
,
sourceCommit
,
"--staged"
,
"--worktree"
,
"--"
,
"."
,
":(exclude)astro.config.mjs"
],
{
cwd
:
projectPath
});
...
...
backend/src/schemas.ts
View file @
f86dd1a6
...
@@ -10,6 +10,11 @@ export const createSiteSchema = z.object({
...
@@ -10,6 +10,11 @@ export const createSiteSchema = z.object({
brandColor
:
z
.
string
().
regex
(
/^#
[
0-9a-fA-F
]{6}
$/
,
"品牌色必须是 6 位十六进制颜色"
),
brandColor
:
z
.
string
().
regex
(
/^#
[
0-9a-fA-F
]{6}
$/
,
"品牌色必须是 6 位十六进制颜色"
),
});
});
export
const
loginSchema
=
z
.
object
({
username
:
z
.
string
().
trim
().
min
(
1
),
password
:
z
.
string
().
min
(
1
),
});
export
const
chatSchema
=
z
.
object
({
export
const
chatSchema
=
z
.
object
({
message
:
z
.
string
().
trim
().
min
(
2
).
max
(
2000
),
message
:
z
.
string
().
trim
().
min
(
2
).
max
(
2000
),
});
});
...
...
backend/src/server.ts
View file @
f86dd1a6
import
path
from
"node:path"
;
import
path
from
"node:path"
;
import
crypto
from
"node:crypto"
;
import
{
access
,
mkdir
,
rm
}
from
"node:fs/promises"
;
import
{
access
,
mkdir
,
rm
}
from
"node:fs/promises"
;
import
Fastify
from
"fastify"
;
import
Fastify
from
"fastify"
;
import
cors
from
"@fastify/cors"
;
import
cors
from
"@fastify/cors"
;
...
@@ -8,7 +9,7 @@ import { BuildManager } from "./build/build-manager.js";
...
@@ -8,7 +9,7 @@ import { BuildManager } from "./build/build-manager.js";
import
{
config
,
runtimePaths
}
from
"./config.js"
;
import
{
config
,
runtimePaths
}
from
"./config.js"
;
import
{
GitManager
}
from
"./git/git-manager.js"
;
import
{
GitManager
}
from
"./git/git-manager.js"
;
import
{
PreviewProcessManager
}
from
"./preview/preview-process-manager.js"
;
import
{
PreviewProcessManager
}
from
"./preview/preview-process-manager.js"
;
import
{
chatSchema
,
createSiteSchema
,
versionCommitSchema
}
from
"./schemas.js"
;
import
{
chatSchema
,
createSiteSchema
,
loginSchema
,
versionCommitSchema
}
from
"./schemas.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
import
{
SiteAgentService
}
from
"./sites/site-agent-service.js"
;
import
{
SiteAgentService
}
from
"./sites/site-agent-service.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
...
@@ -22,10 +23,33 @@ const git = new GitManager();
...
@@ -22,10 +23,33 @@ const git = new GitManager();
const
builds
=
new
BuildManager
();
const
builds
=
new
BuildManager
();
const
previews
=
new
PreviewProcessManager
();
const
previews
=
new
PreviewProcessManager
();
const
createSite
=
new
CreateSiteService
(
sites
,
git
,
builds
,
previews
);
const
createSite
=
new
CreateSiteService
(
sites
,
git
,
builds
,
previews
);
const
siteAgent
=
new
SiteAgentService
(
sites
,
git
,
builds
,
previews
,
new
AgentLoop
());
const
siteAgent
=
new
SiteAgentService
(
sites
,
git
,
new
AgentLoop
());
const
versions
=
new
SiteVersionService
(
sites
,
git
,
builds
,
previews
);
const
versions
=
new
SiteVersionService
(
sites
,
git
,
builds
,
previews
);
const
sessions
=
new
Set
<
string
>
();
app
.
addHook
(
"onRequest"
,
async
(
request
,
reply
)
=>
{
if
(
!
request
.
url
.
startsWith
(
"/api/"
)
||
request
.
url
===
"/api/health"
||
request
.
url
===
"/api/login"
)
return
;
const
token
=
request
.
headers
.
authorization
?.
replace
(
/^Bearer
\s
+/i
,
""
);
if
(
!
token
||
!
sessions
.
has
(
token
))
return
reply
.
code
(
401
).
send
({
error
:
"登录已失效,请重新登录"
});
});
app
.
get
(
"/api/health"
,
async
()
=>
({
status
:
"ok"
,
service
:
"WebAgent"
,
agentMode
:
config
.
openai
.
apiKey
?
"model"
:
"local"
,
timestamp
:
new
Date
().
toISOString
()
}));
app
.
get
(
"/api/health"
,
async
()
=>
({
status
:
"ok"
,
service
:
"WebAgent"
,
agentMode
:
config
.
openai
.
apiKey
?
"model"
:
"local"
,
timestamp
:
new
Date
().
toISOString
()
}));
app
.
post
(
"/api/login"
,
async
(
request
,
reply
)
=>
{
const
body
=
loginSchema
.
parse
(
request
.
body
);
const
digest
=
(
value
:
string
)
=>
crypto
.
createHash
(
"sha256"
).
update
(
value
).
digest
();
const
validUsername
=
crypto
.
timingSafeEqual
(
digest
(
body
.
username
),
digest
(
config
.
auth
.
username
));
const
validPassword
=
crypto
.
timingSafeEqual
(
digest
(
body
.
password
),
digest
(
config
.
auth
.
password
));
if
(
!
validUsername
||
!
validPassword
)
return
reply
.
code
(
401
).
send
({
error
:
"账号或密码错误"
});
const
token
=
crypto
.
randomBytes
(
32
).
toString
(
"hex"
);
sessions
.
add
(
token
);
return
{
token
,
username
:
config
.
auth
.
username
};
});
app
.
post
(
"/api/logout"
,
async
(
request
)
=>
{
const
token
=
request
.
headers
.
authorization
?.
replace
(
/^Bearer
\s
+/i
,
""
);
if
(
token
)
sessions
.
delete
(
token
);
return
{
success
:
true
};
});
app
.
get
(
"/api/session"
,
async
()
=>
({
username
:
config
.
auth
.
username
}));
app
.
get
(
"/api/sites"
,
async
()
=>
sites
.
list
());
app
.
get
(
"/api/sites"
,
async
()
=>
sites
.
list
());
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId"
,
async
(
request
)
=>
sites
.
get
(
request
.
params
.
siteId
));
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId"
,
async
(
request
)
=>
sites
.
get
(
request
.
params
.
siteId
));
app
.
post
(
"/api/sites"
,
async
(
request
,
reply
)
=>
reply
.
code
(
201
).
send
(
await
createSite
.
execute
(
createSiteSchema
.
parse
(
request
.
body
))));
app
.
post
(
"/api/sites"
,
async
(
request
,
reply
)
=>
reply
.
code
(
201
).
send
(
await
createSite
.
execute
(
createSiteSchema
.
parse
(
request
.
body
))));
...
@@ -36,6 +60,7 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/build", async (requ
...
@@ -36,6 +60,7 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/build", async (requ
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/preview-version"
,
async
(
request
)
=>
{
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/preview-version"
,
async
(
request
)
=>
{
const
body
=
versionCommitSchema
.
parse
(
request
.
body
);
return
versions
.
previewVersion
(
request
.
params
.
siteId
,
body
.
commit
);
const
body
=
versionCommitSchema
.
parse
(
request
.
body
);
return
versions
.
previewVersion
(
request
.
params
.
siteId
,
body
.
commit
);
});
});
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/preview-draft"
,
async
(
request
)
=>
versions
.
previewDraft
(
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/publish"
,
async
(
request
)
=>
versions
.
publish
(
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/publish"
,
async
(
request
)
=>
versions
.
publish
(
request
.
params
.
siteId
));
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/history"
,
async
(
request
)
=>
git
.
history
(
sites
.
getProjectPath
(
request
.
params
.
siteId
)));
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/history"
,
async
(
request
)
=>
git
.
history
(
sites
.
getProjectPath
(
request
.
params
.
siteId
)));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/undo"
,
async
(
request
)
=>
versions
.
undo
(
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/undo"
,
async
(
request
)
=>
versions
.
undo
(
request
.
params
.
siteId
));
...
...
backend/src/sites/site-agent-service.ts
View file @
f86dd1a6
import
crypto
from
"node:crypto"
;
import
path
from
"node:path"
;
import
type
{
ChatResult
,
SitePatch
}
from
"@webagent/shared"
;
import
type
{
ChatResult
,
SitePatch
}
from
"@webagent/shared"
;
import
{
getPublicPreviewUrl
,
runtimePaths
}
from
"../config.js"
;
import
{
AgentLoop
}
from
"../agent/agent-loop.js"
;
import
{
AgentLoop
}
from
"../agent/agent-loop.js"
;
import
{
BuildError
,
BuildManager
}
from
"../build/build-manager.js"
;
import
{
GitManager
}
from
"../git/git-manager.js"
;
import
{
GitManager
}
from
"../git/git-manager.js"
;
import
{
PreviewProcessManager
}
from
"../preview/preview-process-manager.js"
;
import
{
validatePatch
}
from
"../security/path-policy.js"
;
import
{
validatePatch
}
from
"../security/path-policy.js"
;
import
{
applyPatch
}
from
"./apply-patch.js"
;
import
{
applyPatch
}
from
"./apply-patch.js"
;
import
{
SiteRepository
}
from
"./site-repository.js"
;
import
{
SiteRepository
}
from
"./site-repository.js"
;
...
@@ -13,56 +8,39 @@ import { SiteRepository } from "./site-repository.js";
...
@@ -13,56 +8,39 @@ import { SiteRepository } from "./site-repository.js";
export
class
SiteAgentService
{
export
class
SiteAgentService
{
constructor
(
constructor
(
private
readonly
sites
:
SiteRepository
,
private
readonly
git
:
GitManager
,
private
readonly
sites
:
SiteRepository
,
private
readonly
git
:
GitManager
,
private
readonly
builds
:
BuildManager
,
private
readonly
previews
:
PreviewProcessManager
,
private
readonly
agent
:
AgentLoop
,
private
readonly
agent
:
AgentLoop
,
)
{}
)
{}
async
execute
(
siteId
:
string
,
message
:
string
):
Promise
<
ChatResult
>
{
async
execute
(
siteId
:
string
,
message
:
string
):
Promise
<
ChatResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
const
site
=
await
this
.
sites
.
get
(
siteId
);
if
(
site
.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
)
{
throw
new
Error
(
"当前测试环境正在预览历史版本,请先切换到最近保存版本再继续修改"
);
}
const
projectPath
=
this
.
sites
.
getProjectPath
(
siteId
);
const
projectPath
=
this
.
sites
.
getProjectPath
(
siteId
);
const
taskId
=
"task_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
workspace
=
this
.
sites
.
getDraftPath
(
siteId
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
const
baseCommit
=
site
.
draftBaseCommit
||
site
.
currentCommit
;
const
creatingDraft
=
!
site
.
draftBaseCommit
;
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
let
generated
:
{
patch
:
SitePatch
;
mode
:
"model"
|
"local"
}
|
undefined
;
let
generated
:
{
patch
:
SitePatch
;
mode
:
"model"
|
"local"
}
|
undefined
;
try
{
try
{
await
this
.
git
.
createWorktree
(
projectPath
,
workspace
);
if
(
creatingDraft
)
await
this
.
git
.
createPersistentWorktree
(
projectPath
,
workspace
,
baseCommit
);
generated
=
await
this
.
agent
.
generate
(
workspace
,
message
);
generated
=
await
this
.
agent
.
generate
(
workspace
,
message
);
await
validatePatch
(
generated
.
patch
,
workspace
);
await
validatePatch
(
generated
.
patch
,
workspace
);
await
applyPatch
(
workspace
,
generated
.
patch
);
await
applyPatch
(
workspace
,
generated
.
patch
);
let
dist
:
string
;
try
{
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicPreviewUrl
(
siteId
));
}
catch
(
error
)
{
if
(
!
(
error
instanceof
BuildError
)
||
generated
.
mode
!==
"model"
)
throw
error
;
const
repair
=
await
this
.
agent
.
repair
(
workspace
,
message
,
error
.
output
);
await
validatePatch
(
repair
,
workspace
);
await
applyPatch
(
workspace
,
repair
);
const
mergedOperations
=
new
Map
(
[...
generated
.
patch
.
operations
,
...
repair
.
operations
].
map
((
operation
)
=>
[
operation
.
path
,
operation
]),
);
if
(
mergedOperations
.
size
>
5
)
throw
new
Error
(
"自动修复后的修改文件总数超过 5 个,任务已安全取消"
);
generated
.
patch
=
{
summary
:
repair
.
summary
,
operations
:
[...
mergedOperations
.
values
()]
};
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
+
"_repair"
,
getPublicPreviewUrl
(
siteId
));
}
const
changedFiles
=
generated
.
patch
.
operations
.
map
((
item
)
=>
item
.
path
);
const
changedFiles
=
generated
.
patch
.
operations
.
map
((
item
)
=>
item
.
path
);
const
worktreeCommit
=
await
this
.
git
.
commitWorktree
(
workspace
,
"Agent: "
+
generated
.
patch
.
summary
,
changedFiles
);
const
updatedAt
=
new
Date
().
toISOString
();
const
commit
=
await
this
.
git
.
cherryPick
(
projectPath
,
worktreeCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
update
(
siteId
,
{
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commi
t
,
status
:
"ready"
,
draftBaseCommit
:
baseCommit
,
draftUpdatedAt
:
updatedA
t
,
previousCommit
:
site
.
currentCommit
,
environmentVersion
:
3
,
lastError
:
undefined
,
draftSummary
:
generated
.
patch
.
summary
,
lastError
:
undefined
,
});
});
return
{
summary
:
generated
.
patch
.
summary
,
commit
,
previewUrl
:
site
.
previewUrl
,
changedFiles
,
mode
:
generated
.
mode
};
return
{
summary
:
generated
.
patch
.
summary
,
draft
:
true
,
baseCommit
,
updatedAt
,
changedFiles
,
mode
:
generated
.
mode
};
}
catch
(
error
)
{
}
catch
(
error
)
{
const
details
=
error
instanceof
BuildError
?
error
.
output
.
slice
(
-
3000
)
:
error
instanceof
Error
?
error
.
message
:
String
(
error
);
const
details
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
});
const
keepDraft
=
!
creatingDraft
||
await
this
.
git
.
hasChanges
(
workspace
).
catch
(()
=>
false
);
if
(
!
keepDraft
)
await
this
.
git
.
removePersistentWorktree
(
projectPath
,
workspace
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
,
draftBaseCommit
:
keepDraft
?
baseCommit
:
undefined
,
draftUpdatedAt
:
keepDraft
?
new
Date
().
toISOString
()
:
undefined
,
});
throw
error
;
throw
error
;
}
finally
{
await
this
.
git
.
removeWorktree
(
projectPath
,
workspace
);
}
}
}
}
}
}
backend/src/sites/site-repository.ts
View file @
f86dd1a6
...
@@ -20,6 +20,10 @@ export class SiteRepository {
...
@@ -20,6 +20,10 @@ export class SiteRepository {
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"project"
);
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"project"
);
}
}
getDraftPath
(
siteId
:
string
):
string
{
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"draft"
);
}
getMetadataPath
(
siteId
:
string
):
string
{
getMetadataPath
(
siteId
:
string
):
string
{
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"metadata"
,
"site.json"
);
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"metadata"
,
"site.json"
);
}
}
...
...
backend/src/sites/site-version-service.ts
View file @
f86dd1a6
...
@@ -35,8 +35,40 @@ export class SiteVersionService {
...
@@ -35,8 +35,40 @@ export class SiteVersionService {
return
{
commit
:
targetCommit
,
previewUrl
:
site
.
previewUrl
};
return
{
commit
:
targetCommit
,
previewUrl
:
site
.
previewUrl
};
}
}
async
previewDraft
(
siteId
:
string
):
Promise
<
PreviewVersionResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
if
(
!
site
.
draftBaseCommit
)
throw
new
Error
(
"当前没有可预览的工作草稿"
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
const
workspace
=
this
.
sites
.
getDraftPath
(
siteId
);
const
taskId
=
"draft_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
draftHead
=
await
this
.
git
.
currentCommit
(
workspace
);
if
(
draftHead
===
site
.
draftBaseCommit
&&
!
await
this
.
git
.
hasChanges
(
workspace
))
throw
new
Error
(
"工作草稿没有实际修改"
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
try
{
await
ensureEnvironmentConfig
(
workspace
);
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicPreviewUrl
(
siteId
));
const
draftCommit
=
await
this
.
git
.
commit
(
workspace
,
"Agent: "
+
(
site
.
draftSummary
||
"保存工作草稿"
));
const
commit
=
await
this
.
git
.
fastForward
(
project
,
draftCommit
,
site
.
draftBaseCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
previousCommit
:
site
.
currentCommit
,
draftBaseCommit
:
undefined
,
draftUpdatedAt
:
undefined
,
draftSummary
:
undefined
,
environmentVersion
:
3
,
lastError
:
undefined
,
});
await
this
.
git
.
removePersistentWorktree
(
project
,
workspace
);
return
{
commit
,
previewUrl
:
site
.
previewUrl
};
}
catch
(
error
)
{
const
details
=
error
instanceof
BuildError
?
error
.
output
.
slice
(
-
3000
)
:
error
instanceof
Error
?
error
.
message
:
String
(
error
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
});
throw
error
;
}
}
async
undo
(
siteId
:
string
):
Promise
<
PreviewVersionResult
>
{
async
undo
(
siteId
:
string
):
Promise
<
PreviewVersionResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
const
site
=
await
this
.
sites
.
get
(
siteId
);
if
(
site
.
draftBaseCommit
)
throw
new
Error
(
"工作草稿尚未预览,请先预览草稿或继续编辑"
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
if
(
site
.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
)
{
if
(
site
.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
)
{
throw
new
Error
(
"当前正在查看历史版本,请先切换回最近保存版本再撤销修改"
);
throw
new
Error
(
"当前正在查看历史版本,请先切换回最近保存版本再撤销修改"
);
...
...
docs/storage-convention.md
View file @
f86dd1a6
...
@@ -6,6 +6,7 @@
...
@@ -6,6 +6,7 @@
WebAgent-sites/
WebAgent-sites/
└── site_xxxxxx/
└── site_xxxxxx/
├── project/ # 独立 Astro 项目,也是独立 Git 仓库
├── project/ # 独立 Astro 项目,也是独立 Git 仓库
├── draft/ # 持久化工作草稿 Git Worktree,不进入正式版本历史
└── metadata/
└── metadata/
└── site.json # WebAgent 管理信息,不交给 Agent 修改
└── site.json # WebAgent 管理信息,不交给 Agent 修改
...
@@ -27,9 +28,9 @@ WebAgent/.runtime/
...
@@ -27,9 +28,9 @@ WebAgent/.runtime/
## 不变量
## 不变量
1.
Agent 只可读取和修改当前站点
`project/`
内的白名单文件。
1.
Agent 只可读取和修改当前站点
草稿 Worktree
内的白名单文件。
2.
Agent 永远不直接修改正式项目
,先在
`builds/`
中
验证。
2.
Agent 永远不直接修改正式项目
;修改自动保存到
`draft/`
,用户选择“预览草稿”后再构建
验证。
3.
只有
构建成功的版本才允许进入站点 Git 历史并成为预览版本
。
3.
只有
草稿构建成功才允许提交到站点 Git 历史并成为预览版本;失败草稿继续保留
。
4.
测试预览更新不能覆盖生产产物;生产环境只能通过显式发布操作更新。
4.
测试预览更新不能覆盖生产产物;生产环境只能通过显式发布操作更新。
5.
`metadata/site.json`
由 WebAgent 独占写入,站点代码不能反向引用它。
5.
`metadata/site.json`
由 WebAgent 独占写入,站点代码不能反向引用它。
6.
删除站点、清理缓存等破坏性操作必须由独立管理接口实现,不能由 Agent Patch 触发。
6.
删除站点、清理缓存等破坏性操作必须由独立管理接口实现,不能由 Agent Patch 触发。
...
...
frontend/src/App.tsx
View file @
f86dd1a6
...
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, type FormEvent } from "react";
...
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, type FormEvent } from "react";
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
import
{
ArrowLeft
,
ArrowRight
,
Bot
,
Check
,
ChevronDown
,
Clock3
,
Code2
,
ExternalLink
,
ArrowLeft
,
ArrowRight
,
Bot
,
Check
,
ChevronDown
,
Clock3
,
Code2
,
ExternalLink
,
Globe2
,
History
,
Laptop
,
LoaderCircle
,
MessageSquareText
,
Monitor
,
Palette
,
Globe2
,
History
,
Laptop
,
LoaderCircle
,
MessageSquareText
,
Monitor
,
Plus
,
RefreshCw
,
Rocket
,
Send
,
Settings2
,
ShieldCheck
,
Smartphone
,
Sparkles
,
Undo2
,
Plus
,
RefreshCw
,
Rocket
,
Send
,
Settings2
,
ShieldCheck
,
Smartphone
,
Sparkles
,
Undo2
,
WandSparkles
,
X
,
WandSparkles
,
X
,
}
from
"lucide-react"
;
}
from
"lucide-react"
;
...
@@ -13,7 +13,9 @@ type ChatMessage = { role: "user" | "agent"; text: string; meta?: string };
...
@@ -13,7 +13,9 @@ type ChatMessage = { role: "user" | "agent"; text: string; meta?: string };
export
default
function
App
()
{
export
default
function
App
()
{
const
queryClient
=
useQueryClient
();
const
queryClient
=
useQueryClient
();
const
sitesQuery
=
useQuery
({
queryKey
:
[
"sites"
],
queryFn
:
api
.
sites
});
const
[
authenticated
,
setAuthenticated
]
=
useState
(()
=>
Boolean
(
sessionStorage
.
getItem
(
"webagent-session"
)));
const
sitesQuery
=
useQuery
({
queryKey
:
[
"sites"
],
queryFn
:
api
.
sites
,
enabled
:
authenticated
});
const
sessionQuery
=
useQuery
({
queryKey
:
[
"session"
],
queryFn
:
api
.
session
,
enabled
:
authenticated
,
retry
:
false
});
const
healthQuery
=
useQuery
({
queryKey
:
[
"health"
],
queryFn
:
api
.
health
});
const
healthQuery
=
useQuery
({
queryKey
:
[
"health"
],
queryFn
:
api
.
health
});
const
[
currentSiteId
,
setCurrentSiteId
]
=
useState
(()
=>
localStorage
.
getItem
(
"webagent-current-site"
)
||
""
);
const
[
currentSiteId
,
setCurrentSiteId
]
=
useState
(()
=>
localStorage
.
getItem
(
"webagent-current-site"
)
||
""
);
const
[
creating
,
setCreating
]
=
useState
(
false
);
const
[
creating
,
setCreating
]
=
useState
(
false
);
...
@@ -24,15 +26,28 @@ export default function App() {
...
@@ -24,15 +26,28 @@ export default function App() {
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
currentSiteId
)
localStorage
.
setItem
(
"webagent-current-site"
,
currentSiteId
);
if
(
currentSiteId
)
localStorage
.
setItem
(
"webagent-current-site"
,
currentSiteId
);
},
[
currentSiteId
]);
},
[
currentSiteId
]);
useEffect
(()
=>
{
if
(
!
sessionQuery
.
isError
)
return
;
sessionStorage
.
removeItem
(
"webagent-session"
);
setAuthenticated
(
false
);
},
[
sessionQuery
.
isError
]);
const
openSite
=
(
site
:
SiteInfo
)
=>
{
const
openSite
=
(
site
:
SiteInfo
)
=>
{
setCurrentSiteId
(
site
.
siteId
);
setCreating
(
false
);
setCurrentSiteId
(
site
.
siteId
);
setCreating
(
false
);
queryClient
.
invalidateQueries
({
queryKey
:
[
"sites"
]
});
queryClient
.
invalidateQueries
({
queryKey
:
[
"sites"
]
});
};
};
const
logout
=
async
()
=>
{
await
api
.
logout
().
catch
(()
=>
undefined
);
sessionStorage
.
removeItem
(
"webagent-session"
);
setAuthenticated
(
false
);
setCreating
(
false
);
queryClient
.
clear
();
};
if
(
!
authenticated
)
return
<
LoginPage
onAuthenticated=
{
()
=>
setAuthenticated
(
true
)
}
/>;
if
(
sitesQuery
.
isLoading
)
return
<
Splash
/>;
if
(
sitesQuery
.
isLoading
)
return
<
Splash
/>;
if
(
creating
||
!
currentSiteId
||
!
sitesQuery
.
data
?.
length
)
{
if
(
creating
||
!
sitesQuery
.
data
?.
length
)
{
return
<
CreateSite
Page
onCreated=
{
openSite
}
onBack=
{
sitesQuery
.
data
?.
length
?
()
=>
setCreating
(
false
)
:
undefined
}
/>;
return
<
CreateSite
Workspace
sites=
{
sitesQuery
.
data
||
[]
}
currentSiteId=
{
currentSiteId
}
onCreated=
{
openSite
}
onCancel=
{
sitesQuery
.
data
?.
length
?
()
=>
setCreating
(
false
)
:
undefined
}
onSelectSite=
{
(
id
)
=>
{
setCurrentSiteId
(
id
);
setCreating
(
false
);
}
}
onLogout=
{
logout
}
/>;
}
}
return
<
Workspace
return
<
Workspace
siteId=
{
currentSiteId
}
siteId=
{
currentSiteId
}
...
@@ -40,6 +55,7 @@ export default function App() {
...
@@ -40,6 +55,7 @@ export default function App() {
agentMode=
{
healthQuery
.
data
?.
agentMode
||
"local"
}
agentMode=
{
healthQuery
.
data
?.
agentMode
||
"local"
}
onSelectSite=
{
setCurrentSiteId
}
onSelectSite=
{
setCurrentSiteId
}
onCreate=
{
()
=>
setCreating
(
true
)
}
onCreate=
{
()
=>
setCreating
(
true
)
}
onLogout=
{
logout
}
/>;
/>;
}
}
...
@@ -51,7 +67,46 @@ function Logo({ compact = false }: { compact?: boolean }) {
...
@@ -51,7 +67,46 @@ function Logo({ compact = false }: { compact?: boolean }) {
return
<
div
className=
"logo"
><
span
className=
"logo-mark"
><
WandSparkles
size=
{
compact
?
15
:
18
}
/></
span
>
{
!
compact
&&
<
span
>
WebAgent
</
span
>
}
</
div
>;
return
<
div
className=
"logo"
><
span
className=
"logo-mark"
><
WandSparkles
size=
{
compact
?
15
:
18
}
/></
span
>
{
!
compact
&&
<
span
>
WebAgent
</
span
>
}
</
div
>;
}
}
function
CreateSitePage
({
onCreated
,
onBack
}:
{
onCreated
:
(
site
:
SiteInfo
)
=>
void
;
onBack
?:
()
=>
void
})
{
function
LoginPage
({
onAuthenticated
}:
{
onAuthenticated
:
()
=>
void
})
{
const
[
username
,
setUsername
]
=
useState
(
"user"
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
mutation
=
useMutation
({
mutationFn
:
()
=>
api
.
login
(
username
,
password
),
onSuccess
:
(
result
)
=>
{
sessionStorage
.
setItem
(
"webagent-session"
,
result
.
token
);
onAuthenticated
();
},
});
return
<
main
className=
"create-page login-page"
>
<
div
className=
"create-orb orb-a"
/><
div
className=
"create-orb orb-b"
/>
<
header
className=
"landing-header"
><
Logo
/><
div
className=
"landing-status"
><
span
/><
span
>
单用户工作台
</
span
></
div
></
header
>
<
div
className=
"create-layout login-layout"
>
<
section
className=
"create-intro"
>
<
div
className=
"intro-tag"
><
Sparkles
size=
{
14
}
/>
AI WEBSITE WORKSPACE
</
div
>
<
h1
>
一句话描述,
<
br
/>
让官网
<
span
>
自然生长。
</
span
></
h1
>
<
p
>
登录你的官网工作台,在一个空间内完成创建、编辑、版本预览与上线。
</
p
>
<
div
className=
"feature-row"
>
<
div
><
span
><
Bot
size=
{
18
}
/></
span
><
strong
>
Agent 自由创作
</
strong
><
small
>
从文案到布局,持续迭代
</
small
></
div
>
<
div
><
span
><
ShieldCheck
size=
{
18
}
/></
span
><
strong
>
草稿自动保存
</
strong
><
small
>
未完成修改随时继续
</
small
></
div
>
<
div
><
span
><
History
size=
{
18
}
/></
span
><
strong
>
版本随时回看
</
strong
><
small
>
测试与线上相互隔离
</
small
></
div
>
</
div
>
</
section
>
<
section
className=
"create-card-wrap login-card-wrap"
>
<
form
className=
"create-card login-card"
onSubmit=
{
(
event
)
=>
{
event
.
preventDefault
();
mutation
.
mutate
();
}
}
>
<
div
className=
"login-card-icon"
><
ShieldCheck
size=
{
20
}
/></
div
>
<
div
className=
"card-heading"
><
div
><
span
>
WELCOME BACK
</
span
><
h2
>
登录工作台
</
h2
></
div
></
div
>
<
p
className=
"login-copy"
>
使用管理员账号继续管理你的官网。
</
p
>
<
div
className=
"login-fields"
>
<
Field
label=
"账号"
><
input
required
autoComplete=
"username"
value=
{
username
}
onChange=
{
(
event
)
=>
setUsername
(
event
.
target
.
value
)
}
/></
Field
>
<
Field
label=
"密码"
><
input
required
type=
"password"
autoComplete=
"current-password"
placeholder=
"请输入密码"
value=
{
password
}
onChange=
{
(
event
)
=>
setPassword
(
event
.
target
.
value
)
}
/></
Field
>
</
div
>
{
mutation
.
error
&&
<
div
className=
"form-error"
><
X
size=
{
15
}
/>
{
mutation
.
error
.
message
}
</
div
>
}
<
button
className=
"create-submit login-submit"
disabled=
{
mutation
.
isPending
}
>
{
mutation
.
isPending
?
<><
LoaderCircle
className=
"spin"
size=
{
18
}
/>
正在登录…
</>
:
<>
进入工作台
<
ArrowRight
size=
{
18
}
/></>
}
</
button
>
<
p
className=
"local-note"
><
ShieldCheck
size=
{
13
}
/>
本地单用户安全登录
</
p
>
</
form
>
</
section
>
</
div
>
</
main
>;
}
function
CreateSiteForm
({
onCreated
,
onCancel
}:
{
onCreated
:
(
site
:
SiteInfo
)
=>
void
;
onCancel
?:
()
=>
void
})
{
const
[
form
,
setForm
]
=
useState
<
CreateSiteInput
>
({
const
[
form
,
setForm
]
=
useState
<
CreateSiteInput
>
({
name
:
"云启科技"
,
industry
:
"企业数字化服务"
,
description
:
"专注于为成长型企业提供智能化、可持续的数字解决方案,让技术真正服务于业务增长。"
,
name
:
"云启科技"
,
industry
:
"企业数字化服务"
,
description
:
"专注于为成长型企业提供智能化、可持续的数字解决方案,让技术真正服务于业务增长。"
,
services
:
[
"数字化咨询"
,
"智能产品开发"
,
"品牌体验设计"
],
email
:
"hello@yunqi.example"
,
phone
:
"400-800-2026"
,
brandColor
:
"#7028FF"
,
services
:
[
"数字化咨询"
,
"智能产品开发"
,
"品牌体验设计"
],
email
:
"hello@yunqi.example"
,
phone
:
"400-800-2026"
,
brandColor
:
"#7028FF"
,
...
@@ -63,24 +118,9 @@ function CreateSitePage({ onCreated, onBack }: { onCreated: (site: SiteInfo) =>
...
@@ -63,24 +118,9 @@ function CreateSitePage({ onCreated, onBack }: { onCreated: (site: SiteInfo) =>
mutation
.
mutate
({
...
form
,
services
:
serviceText
.
split
(
/
[
、,,
\n]
/
).
map
((
item
)
=>
item
.
trim
()).
filter
(
Boolean
)
});
mutation
.
mutate
({
...
form
,
services
:
serviceText
.
split
(
/
[
、,,
\n]
/
).
map
((
item
)
=>
item
.
trim
()).
filter
(
Boolean
)
});
};
};
return
<
main
className=
"create-page"
>
return
<
section
className=
"workspace-create-content"
>
<
div
className=
"create-orb orb-a"
/><
div
className=
"create-orb orb-b"
/>
<
form
className=
"create-card workspace-create-card"
onSubmit=
{
submit
}
>
<
header
className=
"landing-header"
><
Logo
/><
div
className=
"landing-status"
><
span
/><
span
>
本地安全运行
</
span
></
div
></
header
>
<
div
className=
"card-heading"
><
div
><
span
>
创建新网站
</
span
><
h2
>
告诉我你的企业信息
</
h2
><
p
>
填写基础信息,Agent 将生成首个可编辑版本。
</
p
></
div
>
{
onCancel
&&
<
button
className=
"workspace-create-cancel"
type=
"button"
onClick=
{
onCancel
}
><
X
size=
{
15
}
/>
取消
</
button
>
}
</
div
>
<
div
className=
"create-layout"
>
<
section
className=
"create-intro"
>
{
onBack
&&
<
button
className=
"back-button"
onClick=
{
onBack
}
><
ArrowLeft
size=
{
15
}
/>
返回工作台
</
button
>
}
<
div
className=
"intro-tag"
><
Sparkles
size=
{
14
}
/>
AI WEBSITE WORKSPACE
</
div
>
<
h1
>
一句话描述,
<
br
/>
让官网
<
span
>
自然生长。
</
span
></
h1
>
<
p
>
创建一个真实、完整、可持续维护的 Astro 项目。每次修改都经过构建验证,并由本地 Git 安全记录。
</
p
>
<
div
className=
"feature-row"
>
<
div
><
span
><
Bot
size=
{
18
}
/></
span
><
strong
>
Agent 自由创作
</
strong
><
small
>
从文案到布局,持续迭代
</
small
></
div
>
<
div
><
span
><
ShieldCheck
size=
{
18
}
/></
span
><
strong
>
构建后再生效
</
strong
><
small
>
失败修改不会污染项目
</
small
></
div
>
<
div
><
span
><
History
size=
{
18
}
/></
span
><
strong
>
版本随时回滚
</
strong
><
small
>
每次有效修改都有记录
</
small
></
div
>
</
div
>
</
section
>
<
section
className=
"create-card-wrap"
>
<
form
className=
"create-card"
onSubmit=
{
submit
}
>
<
div
className=
"card-heading"
><
div
><
span
>
创建新网站
</
span
><
h2
>
告诉我你的企业信息
</
h2
></
div
><
div
className=
"step-pill"
>
01 / 01
</
div
></
div
>
<
div
className=
"form-grid"
>
<
div
className=
"form-grid"
>
<
Field
label=
"企业名称"
><
input
required
value=
{
form
.
name
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
name
:
e
.
target
.
value
})
}
/></
Field
>
<
Field
label=
"企业名称"
><
input
required
value=
{
form
.
name
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
name
:
e
.
target
.
value
})
}
/></
Field
>
<
Field
label=
"所属行业"
><
input
required
value=
{
form
.
industry
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
industry
:
e
.
target
.
value
})
}
/></
Field
>
<
Field
label=
"所属行业"
><
input
required
value=
{
form
.
industry
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
industry
:
e
.
target
.
value
})
}
/></
Field
>
...
@@ -94,17 +134,52 @@ function CreateSitePage({ onCreated, onBack }: { onCreated: (site: SiteInfo) =>
...
@@ -94,17 +134,52 @@ function CreateSitePage({ onCreated, onBack }: { onCreated: (site: SiteInfo) =>
</
div
>
</
div
>
{
mutation
.
error
&&
<
div
className=
"form-error"
><
X
size=
{
15
}
/>
{
mutation
.
error
.
message
}
</
div
>
}
{
mutation
.
error
&&
<
div
className=
"form-error"
><
X
size=
{
15
}
/>
{
mutation
.
error
.
message
}
</
div
>
}
<
button
className=
"create-submit"
disabled=
{
mutation
.
isPending
}
>
{
mutation
.
isPending
?
<><
LoaderCircle
className=
"spin"
size=
{
18
}
/>
正在创建、构建并启动预览…
</>
:
<>
生成我的企业官网
<
ArrowRight
size=
{
18
}
/></>
}
</
button
>
<
button
className=
"create-submit"
disabled=
{
mutation
.
isPending
}
>
{
mutation
.
isPending
?
<><
LoaderCircle
className=
"spin"
size=
{
18
}
/>
正在创建、构建并启动预览…
</>
:
<>
生成我的企业官网
<
ArrowRight
size=
{
18
}
/></>
}
</
button
>
<
p
className=
"local-note"
><
ShieldCheck
size=
{
13
}
/>
项目与版本记录仅保存在当前设备
</
p
>
<
p
className=
"local-note"
><
ShieldCheck
size=
{
13
}
/>
创建完成后将直接进入工作台编辑
</
p
>
</
form
>
</
form
>
</
section
>
</
section
>;
</
div
>
</
main
>;
}
}
function
Field
({
label
,
hint
,
wide
,
children
}:
{
label
:
string
;
hint
?:
string
;
wide
?:
boolean
;
children
:
React
.
ReactNode
})
{
function
Field
({
label
,
hint
,
wide
,
children
}:
{
label
:
string
;
hint
?:
string
;
wide
?:
boolean
;
children
:
React
.
ReactNode
})
{
return
<
label
className=
{
wide
?
"field wide"
:
"field"
}
><
span
>
{
label
}{
hint
&&
<
small
>
{
hint
}
</
small
>
}
</
span
>
{
children
}
</
label
>;
return
<
label
className=
{
wide
?
"field wide"
:
"field"
}
><
span
>
{
label
}{
hint
&&
<
small
>
{
hint
}
</
small
>
}
</
span
>
{
children
}
</
label
>;
}
}
function
RailSettings
({
onLogout
}:
{
onLogout
:
()
=>
void
})
{
const
[
open
,
setOpen
]
=
useState
(
false
);
const
settingsRef
=
useRef
<
HTMLDivElement
>
(
null
);
useEffect
(()
=>
{
if
(
!
open
)
return
;
const
close
=
(
event
:
PointerEvent
)
=>
{
if
(
event
.
target
instanceof
Node
&&
!
settingsRef
.
current
?.
contains
(
event
.
target
))
setOpen
(
false
);
};
document
.
addEventListener
(
"pointerdown"
,
close
);
return
()
=>
document
.
removeEventListener
(
"pointerdown"
,
close
);
},
[
open
]);
return
<
div
className=
{
`rail-settings ${open ? "open" : ""}`
}
ref=
{
settingsRef
}
>
<
button
className=
"rail-settings-trigger"
type=
"button"
title=
"设置"
data
-
tooltip=
"设置"
aria
-
label=
"设置"
aria
-
expanded=
{
open
}
onClick=
{
()
=>
setOpen
((
value
)
=>
!
value
)
}
><
Settings2
size=
{
18
}
/></
button
>
{
open
&&
<
div
className=
"rail-settings-menu"
>
<
div
><
span
className=
"settings-avatar"
>
U
</
span
><
span
><
strong
>
user
</
strong
><
small
>
管理员账号
</
small
></
span
></
div
>
<
button
type=
"button"
onClick=
{
onLogout
}
><
ArrowLeft
size=
{
14
}
/>
退出登录
</
button
>
</
div
>
}
</
div
>;
}
function
CreateSiteWorkspace
({
sites
,
currentSiteId
,
onCreated
,
onCancel
,
onSelectSite
,
onLogout
}:
{
sites
:
SiteInfo
[];
currentSiteId
:
string
;
onCreated
:
(
site
:
SiteInfo
)
=>
void
;
onCancel
?:
()
=>
void
;
onSelectSite
:
(
id
:
string
)
=>
void
;
onLogout
:
()
=>
void
})
{
const
selectedSite
=
sites
.
find
((
site
)
=>
site
.
siteId
===
currentSiteId
)
||
sites
[
0
];
return
<
main
className=
"workspace create-workspace"
>
<
aside
className=
"rail"
>
<
Logo
compact
/>
<
button
className=
"rail-create-top active"
title=
"创建官网"
data
-
tooltip=
"创建官网"
aria
-
label=
"创建官网"
><
Plus
size=
{
18
}
/></
button
>
<
div
className=
"rail-nav"
><
button
title=
"Agent 工作台"
data
-
tooltip=
"Agent 工作台"
aria
-
label=
"Agent 工作台"
onClick=
{
onCancel
}
><
Monitor
size=
{
18
}
/></
button
></
div
>
<
RailSettings
onLogout=
{
onLogout
}
/>
</
aside
>
<
section
className=
"control-panel create-control-panel"
>
{
selectedSite
?
<
SiteSwitcher
site=
{
selectedSite
}
sites=
{
sites
}
onSelect=
{
onSelectSite
}
/>
:
<
div
className=
"empty-site-heading"
><
Logo
/><
small
>
创建你的第一个官网
</
small
></
div
>
}
<
div
className=
"create-side-copy"
><
span
><
Sparkles
size=
{
14
}
/></
span
><
h2
>
创建新官网
</
h2
><
p
>
填写企业的基础资料,系统会生成独立项目、测试环境与版本记录。
</
p
><
ul
><
li
>
独立 Astro 项目
</
li
><
li
>
自动创建 Git 版本
</
li
><
li
>
生成后直接进入工作台
</
li
></
ul
></
div
>
</
section
>
<
CreateSiteForm
onCreated=
{
onCreated
}
onCancel=
{
onCancel
}
/>
</
main
>;
}
function
SiteSwitcher
({
site
,
sites
,
onSelect
}:
{
site
:
SiteInfo
;
sites
:
SiteInfo
[];
onSelect
:
(
id
:
string
)
=>
void
})
{
function
SiteSwitcher
({
site
,
sites
,
onSelect
}:
{
site
:
SiteInfo
;
sites
:
SiteInfo
[];
onSelect
:
(
id
:
string
)
=>
void
})
{
const
[
open
,
setOpen
]
=
useState
(
false
);
const
[
open
,
setOpen
]
=
useState
(
false
);
const
switcherRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
switcherRef
=
useRef
<
HTMLDivElement
>
(
null
);
...
@@ -149,7 +224,7 @@ function SiteSwitcher({ site, sites, onSelect }: { site: SiteInfo; sites: SiteIn
...
@@ -149,7 +224,7 @@ function SiteSwitcher({ site, sites, onSelect }: { site: SiteInfo; sites: SiteIn
</
div
>;
</
div
>;
}
}
function
Workspace
({
siteId
,
sites
,
agentMode
,
onSelectSite
,
onCreate
}:
{
siteId
:
string
;
sites
:
SiteInfo
[];
agentMode
:
"model"
|
"local"
;
onSelectSite
:
(
id
:
string
)
=>
void
;
onCreate
:
()
=>
void
})
{
function
Workspace
({
siteId
,
sites
,
agentMode
,
onSelectSite
,
onCreate
,
onLogout
}:
{
siteId
:
string
;
sites
:
SiteInfo
[];
agentMode
:
"model"
|
"local"
;
onSelectSite
:
(
id
:
string
)
=>
void
;
onCreate
:
()
=>
void
;
onLogout
:
()
=>
void
})
{
const
queryClient
=
useQueryClient
();
const
queryClient
=
useQueryClient
();
const
siteQuery
=
useQuery
({
queryKey
:
[
"site"
,
siteId
],
queryFn
:
()
=>
api
.
site
(
siteId
)
});
const
siteQuery
=
useQuery
({
queryKey
:
[
"site"
,
siteId
],
queryFn
:
()
=>
api
.
site
(
siteId
)
});
const
historyQuery
=
useQuery
({
queryKey
:
[
"history"
,
siteId
],
queryFn
:
()
=>
api
.
history
(
siteId
)
});
const
historyQuery
=
useQuery
({
queryKey
:
[
"history"
,
siteId
],
queryFn
:
()
=>
api
.
history
(
siteId
)
});
...
@@ -160,7 +235,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -160,7 +235,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
const
[
input
,
setInput
]
=
useState
(
""
);
const
[
input
,
setInput
]
=
useState
(
""
);
const
[
iframeVersion
,
setIframeVersion
]
=
useState
(
0
);
const
[
iframeVersion
,
setIframeVersion
]
=
useState
(
0
);
const
[
messages
,
setMessages
]
=
useState
<
ChatMessage
[]
>
([
const
[
messages
,
setMessages
]
=
useState
<
ChatMessage
[]
>
([
{
role
:
"agent"
,
text
:
"网站已准备好。告诉我你想调整的内容、配色或页面结构,
我会先在隔离环境中验证,
再更新预览。"
,
meta
:
agentMode
===
"model"
?
"模型 Agent 已连接"
:
"本地演示 Agent"
},
{
role
:
"agent"
,
text
:
"网站已准备好。告诉我你想调整的内容、配色或页面结构,
修改会自动保存为工作草稿,由你确认后
再更新预览。"
,
meta
:
agentMode
===
"model"
?
"模型 Agent 已连接"
:
"本地演示 Agent"
},
]);
]);
const
scrollRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
scrollRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
site
=
siteQuery
.
data
;
const
site
=
siteQuery
.
data
;
...
@@ -184,7 +259,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -184,7 +259,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
const
chatMutation
=
useMutation
({
const
chatMutation
=
useMutation
({
mutationFn
:
(
message
:
string
)
=>
api
.
chat
(
siteId
,
message
),
mutationFn
:
(
message
:
string
)
=>
api
.
chat
(
siteId
,
message
),
onSuccess
:
async
(
result
)
=>
{
onSuccess
:
async
(
result
)
=>
{
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
result
.
summary
,
meta
:
(
result
.
mode
===
"model"
?
"AI 生成"
:
"本地规则"
)
+
" ·
已构建 · "
+
result
.
commit
.
slice
(
0
,
7
)
}]);
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
result
.
summary
,
meta
:
(
result
.
mode
===
"model"
?
"AI 生成"
:
"本地规则"
)
+
" ·
草稿已自动保存"
}]);
await
refreshData
();
await
refreshData
();
},
},
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"修改未生效,原版本保持可用"
}]),
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"修改未生效,原版本保持可用"
}]),
...
@@ -209,6 +284,15 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -209,6 +284,15 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
},
},
onError
:
(
error
)
=>
{
setPendingPreviewCommit
(
""
);
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"版本预览失败,原测试版本保持可用"
}]);
},
onError
:
(
error
)
=>
{
setPendingPreviewCommit
(
""
);
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"版本预览失败,原测试版本保持可用"
}]);
},
});
});
const
previewDraftMutation
=
useMutation
({
mutationFn
:
()
=>
api
.
previewDraft
(
siteId
),
onSuccess
:
async
(
result
)
=>
{
setEnvironment
(
"preview"
);
await
refreshData
();
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
"工作草稿已构建并保存为新版本。"
,
meta
:
"测试版本 · "
+
result
.
commit
.
slice
(
0
,
7
)
}]);
},
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"草稿仍已保留,可以继续编辑"
}]),
});
const
undoMutation
=
useMutation
({
const
undoMutation
=
useMutation
({
mutationFn
:
()
=>
api
.
undo
(
siteId
),
mutationFn
:
()
=>
api
.
undo
(
siteId
),
onSuccess
:
async
(
result
)
=>
{
onSuccess
:
async
(
result
)
=>
{
...
@@ -228,10 +312,10 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -228,10 +312,10 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"发布失败,原生产版本保持可用"
}]),
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"发布失败,原生产版本保持可用"
}]),
});
});
const
send
=
(
message
=
input
)
=>
{
const
send
=
(
message
=
input
)
=>
{
const
value
=
message
.
trim
();
if
(
!
value
||
chatMutation
.
isPending
||
historicalPreview
)
return
;
const
value
=
message
.
trim
();
if
(
!
value
||
chatMutation
.
isPending
||
(
historicalPreview
&&
!
site
?.
draftBaseCommit
)
)
return
;
setMessages
((
items
)
=>
[...
items
,
{
role
:
"user"
,
text
:
value
}]);
setInput
(
""
);
chatMutation
.
mutate
(
value
);
setMessages
((
items
)
=>
[...
items
,
{
role
:
"user"
,
text
:
value
}]);
setInput
(
""
);
chatMutation
.
mutate
(
value
);
};
};
const
busy
=
chatMutation
.
isPending
||
rebuildMutation
.
isPending
||
previewVersionMutation
.
isPending
||
undoMutation
.
isPending
||
publishMutation
.
isPending
;
const
busy
=
chatMutation
.
isPending
||
rebuildMutation
.
isPending
||
previewVersionMutation
.
isPending
||
previewDraftMutation
.
isPending
||
undoMutation
.
isPending
||
publishMutation
.
isPending
;
if
(
!
site
)
return
<
Splash
/>;
if
(
!
site
)
return
<
Splash
/>;
const
previewVersion
=
historyQuery
.
data
?.
find
((
item
)
=>
item
.
hash
===
site
.
previewCommit
);
const
previewVersion
=
historyQuery
.
data
?.
find
((
item
)
=>
item
.
hash
===
site
.
previewCommit
);
...
@@ -245,15 +329,12 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -245,15 +329,12 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
const
environmentStatus
=
showingProduction
const
environmentStatus
=
showingProduction
?
site
.
lastPublishError
?
"上线失败 · 线上未变"
:
!
site
.
publishedCommit
?
"尚未上线"
:
productionMatchesPreview
?
"生产已同步"
:
"生产版本较旧"
?
site
.
lastPublishError
?
"上线失败 · 线上未变"
:
!
site
.
publishedCommit
?
"尚未上线"
:
productionMatchesPreview
?
"生产已同步"
:
"生产版本较旧"
:
site
.
lastError
?
"构建失败 · 预览未变"
:
site
.
status
===
"ready"
?
"测试已同步"
:
"需要检查"
;
:
site
.
lastError
?
"构建失败 · 预览未变"
:
site
.
status
===
"ready"
?
"测试已同步"
:
"需要检查"
;
const
viewingLabel
=
showingProduction
?
site
.
publishedCommit
?
`线上版本
${
site
.
publishedCommit
.
slice
(
0
,
7
)}
`
:
"尚未上线"
:
historicalPreview
?
`历史版本
${
site
.
previewCommit
?.
slice
(
0
,
7
)}
` : `
测试版本
$
{
site
.
previewCommit
?.
slice
(
0
,
7
)
||
"-------"
}
`;
const savedState = chatMutation.isPending ? "正在修改" : site.lastError ? "构建失败" : "已保存";
return
<
main
className=
"workspace"
>
return
<
main
className=
"workspace"
>
<
aside
className=
"rail"
>
<
aside
className=
"rail"
>
<
Logo
compact
/>
<
Logo
compact
/>
<div className="rail-nav"><button className="active" title="网站工作台"><Monitor size={18} /></button><button title="项目设置"><Settings2 size={18} /></button></div>
<
button
className=
"rail-create-top"
onClick=
{
onCreate
}
title=
"创建官网"
data
-
tooltip=
"创建官网"
aria
-
label=
"创建官网"
><
Plus
size=
{
18
}
/></
button
>
<button className="rail-create" onClick={onCreate} title="创建新网站"><Plus size={18} /></button>
<
div
className=
"rail-nav"
><
button
className=
"active"
title=
"Agent 工作台"
data
-
tooltip=
"Agent 工作台"
aria
-
label=
"Agent 工作台"
><
Monitor
size=
{
18
}
/></
button
></
div
>
<
RailSettings
onLogout=
{
onLogout
}
/>
</
aside
>
</
aside
>
<
section
className=
"control-panel"
>
<
section
className=
"control-panel"
>
<
SiteSwitcher
site=
{
site
}
sites=
{
sites
}
onSelect=
{
onSelectSite
}
/>
<
SiteSwitcher
site=
{
site
}
sites=
{
sites
}
onSelect=
{
onSelectSite
}
/>
...
@@ -265,17 +346,21 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -265,17 +346,21 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
{
message
.
role
===
"agent"
&&
<
span
className=
"agent-avatar"
><
Sparkles
size=
{
14
}
/></
span
>
}
{
message
.
role
===
"agent"
&&
<
span
className=
"agent-avatar"
><
Sparkles
size=
{
14
}
/></
span
>
}
<
div
><
p
>
{
message
.
text
}
</
p
>
{
message
.
meta
&&
<
small
><
Check
size=
{
11
}
/>
{
message
.
meta
}
</
small
>
}
</
div
>
<
div
><
p
>
{
message
.
text
}
</
p
>
{
message
.
meta
&&
<
small
><
Check
size=
{
11
}
/>
{
message
.
meta
}
</
small
>
}
</
div
>
</
div
>)
}
</
div
>)
}
{chatMutation.isPending && <div className="agent-progress"><span className="agent-avatar"><Sparkles size={14} /></span><div><p><LoaderCircle className="spin" size={14} /> Agent 正在
设计和验证</p><div className="progress-track"><i /></div><small>读取文件 → 生成 Patch → 构建检查
</small></div></div>}
{
chatMutation
.
isPending
&&
<
div
className=
"agent-progress"
><
span
className=
"agent-avatar"
><
Sparkles
size=
{
14
}
/></
span
><
div
><
p
><
LoaderCircle
className=
"spin"
size=
{
14
}
/>
Agent 正在
修改工作草稿
</
p
><
div
className=
"progress-track"
><
i
/></
div
><
small
>
读取文件 → 生成修改 → 自动保存草稿
</
small
></
div
></
div
>
}
{
messages
.
length
<=
1
&&
<
div
className=
"suggestions"
><
span
>
试试这些修改
</
span
>
{
[
"把网站主色改成深蓝色"
,
"增加三个服务卡片"
,
"增加一个企业优势"
].
map
((
text
)
=>
<
button
key=
{
text
}
onClick=
{
()
=>
send
(
text
)
}
><
Sparkles
size=
{
12
}
/>
{
text
}
</
button
>)
}
</
div
>
}
{
messages
.
length
<=
1
&&
<
div
className=
"suggestions"
><
span
>
试试这些修改
</
span
>
{
[
"把网站主色改成深蓝色"
,
"增加三个服务卡片"
,
"增加一个企业优势"
].
map
((
text
)
=>
<
button
key=
{
text
}
onClick=
{
()
=>
send
(
text
)
}
><
Sparkles
size=
{
12
}
/>
{
text
}
</
button
>)
}
</
div
>
}
</
div
>
</
div
>
{historicalPreview && <div className="history-edit-lock"><div><History size={14} /><span>
当前是只读历史预览;切换到“最近保存”版本后才能继续修改
</span></div></div>}
{
historicalPreview
&&
<
div
className=
"history-edit-lock"
><
div
><
History
size=
{
14
}
/><
span
>
{
site
.
draftBaseCommit
?
"正在查看旧版本;工作草稿已保留,可以继续编辑或预览草稿"
:
"当前是只读历史预览;切换到“最近保存”版本后才能继续修改"
}
</
span
></
div
></
div
>
}
<
div
className=
"composer"
>
<
div
className=
"composer"
>
<textarea rows={3} value={input} disabled={busy ||
historicalPreview} placeholder={historicalPreview ? "切换到最近保存版本后才能继续
修改…" : "描述你想要的修改,例如:让首屏更有科技感…"} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} />
<
textarea
rows=
{
3
}
value=
{
input
}
disabled=
{
busy
||
(
historicalPreview
&&
!
site
.
draftBaseCommit
)
}
placeholder=
{
historicalPreview
&&
!
site
.
draftBaseCommit
?
"切换到最近保存版本后才能继续修改…"
:
site
.
draftBaseCommit
?
"继续描述对工作草稿的
修改…"
:
"描述你想要的修改,例如:让首屏更有科技感…"
}
onChange=
{
(
e
)
=>
setInput
(
e
.
target
.
value
)
}
onKeyDown=
{
(
e
)
=>
{
if
(
e
.
key
===
"Enter"
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
();
send
();
}
}
}
/>
<div><span>{
agentMode === "model" ? <><span className="online-dot" />模型 Agent</> : <><Code2 size={12} />本地演示模式</>}</span><button onClick={() => send()} disabled={!input.trim() || busy || historicalPreview
}><Send size={15} /></button></div>
<
div
><
span
>
{
site
.
draftBaseCommit
?
<><
span
className=
"draft-dot"
/>
工作草稿自动保存
</>
:
agentMode
===
"model"
?
<><
span
className=
"online-dot"
/>
模型 Agent
</>
:
<><
Code2
size=
{
12
}
/>
本地演示模式
</>
}
</
span
><
button
onClick=
{
()
=>
send
()
}
disabled=
{
!
input
.
trim
()
||
busy
||
(
historicalPreview
&&
!
site
.
draftBaseCommit
)
}
><
Send
size=
{
15
}
/></
button
></
div
>
</
div
>
</
div
>
</>
:
<
div
className=
"history-list"
>
</>
:
<
div
className=
"history-list"
>
<
div
className=
"history-heading"
><
div
><
h3
>
测试版本
</
h3
><
p
>
点击右侧“预览”切换测试版本,生产环境不会改变
</
p
></
div
><
span
>
{
historyQuery
.
data
?.
length
||
0
}
</
span
></
div
>
<
div
className=
"history-heading"
><
div
><
h3
>
测试版本
</
h3
><
p
>
点击右侧“预览”切换测试版本,生产环境不会改变
</
p
></
div
><
span
>
{
historyQuery
.
data
?.
length
||
0
}
</
span
></
div
>
{historicalPreview && <div className="history-edit-lock history-panel-lock"><div><History size={14} /><span>历史版本只能预览或上线;切换到“最近保存”版本后才能继续修改</span></div></div>}
{
site
.
draftBaseCommit
&&
<
section
className=
"draft-card"
>
<
div
className=
"draft-card-heading"
><
span
><
Sparkles
size=
{
13
}
/></
span
><
div
><
strong
>
工作草稿
</
strong
><
small
>
自动保存于
{
site
.
draftUpdatedAt
?
formatTime
(
site
.
draftUpdatedAt
)
:
"刚刚"
}
· 基于
{
site
.
draftBaseCommit
.
slice
(
0
,
7
)
}
</
small
></
div
></
div
>
<
div
className=
"draft-card-actions"
><
button
type=
"button"
onClick=
{
()
=>
setTab
(
"chat"
)
}
><
MessageSquareText
size=
{
11
}
/>
继续编辑
</
button
><
button
type=
"button"
disabled=
{
busy
}
onClick=
{
()
=>
previewDraftMutation
.
mutate
()
}
>
{
previewDraftMutation
.
isPending
?
<
LoaderCircle
className=
"spin"
size=
{
11
}
/>
:
<
Monitor
size=
{
11
}
/>
}{
previewDraftMutation
.
isPending
?
"构建中"
:
"预览草稿"
}
</
button
></
div
>
</
section
>
}
{
historicalPreview
&&
<
div
className=
"history-edit-lock history-panel-lock"
><
div
><
History
size=
{
14
}
/><
span
>
{
site
.
draftBaseCommit
?
"正在查看旧版本,工作草稿已保留"
:
"历史版本只能预览或上线;切换到“最近保存”版本后才能继续修改"
}
</
span
></
div
></
div
>
}
{
historyQuery
.
data
?.
map
((
item
,
index
)
=>
{
{
historyQuery
.
data
?.
map
((
item
,
index
)
=>
{
const
isPreview
=
item
.
hash
===
site
.
previewCommit
;
const
isPreview
=
item
.
hash
===
site
.
previewCommit
;
const
isProduction
=
item
.
hash
===
site
.
publishedCommit
;
const
isProduction
=
item
.
hash
===
site
.
publishedCommit
;
...
@@ -295,7 +380,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -295,7 +380,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
</
section
>
</
section
>
<
section
className=
"preview-shell"
>
<
section
className=
"preview-shell"
>
<
header
className=
"preview-toolbar"
>
<
header
className=
"preview-toolbar"
>
<div className="preview-title"><div><span className={`
status
-
dot
$
{
showingProduction
?
"production"
:
""
}
`} /><strong>{site.name}</strong><em className={showingProduction ? "production" : ""}>{showingProduction ? "生产环境" : "测试环境"}</em></div><span className={`
build
-
status
$
{
showingProduction
?
site
.
lastPublishError
?
"failed"
:
site
.
publishStatus
:
site
.
lastError
?
"failed"
:
site
.
status
}
`}>{busy ? <LoaderCircle className="spin" size={12} /> : showingProduction ? site.lastPublishError ? <X size={12} /> : <Check size={12} /> : site.lastError ? <X size={12} /> : <Check size={12} />}{publishMutation.isPending ? "正在发布" :
chatMutation.isPending || rebuildMutation.isPending || previewVersionMutation.isPending || undoMutation.isPending ? "正在构建
" : environmentStatus}</span></div>
<
div
className=
"preview-title"
><
div
><
span
className=
{
`status-dot ${showingProduction ? "production" : ""}`
}
/><
strong
>
{
site
.
name
}
</
strong
><
em
className=
{
showingProduction
?
"production"
:
""
}
>
{
showingProduction
?
"生产环境"
:
"测试环境"
}
</
em
></
div
><
span
className=
{
`build-status ${showingProduction ? site.lastPublishError ? "failed" : site.publishStatus : site.lastError ? "failed" : site.status}`
}
>
{
busy
?
<
LoaderCircle
className=
"spin"
size=
{
12
}
/>
:
showingProduction
?
site
.
lastPublishError
?
<
X
size=
{
12
}
/>
:
<
Check
size=
{
12
}
/>
:
site
.
lastError
?
<
X
size=
{
12
}
/>
:
<
Check
size=
{
12
}
/>
}{
publishMutation
.
isPending
?
"正在发布"
:
previewDraftMutation
.
isPending
?
"正在构建草稿"
:
chatMutation
.
isPending
?
"正在修改草稿"
:
rebuildMutation
.
isPending
||
previewVersionMutation
.
isPending
||
undoMutation
.
isPending
?
"正在构建"
:
!
showingProduction
&&
site
.
draftBaseCommit
?
"草稿已保存
"
:
environmentStatus
}
</
span
></
div
>
<
div
className=
"preview-modes"
>
<
div
className=
"preview-modes"
>
<
div
className=
"environment-toggle"
><
button
className=
{
!
showingProduction
?
"active"
:
""
}
onClick=
{
()
=>
setEnvironment
(
"preview"
)
}
><
Monitor
size=
{
13
}
/>
测试预览
</
button
><
button
className=
{
showingProduction
?
"active"
:
""
}
onClick=
{
()
=>
setEnvironment
(
"production"
)
}
><
Globe2
size=
{
13
}
/>
生产站点
</
button
></
div
>
<
div
className=
"environment-toggle"
><
button
className=
{
!
showingProduction
?
"active"
:
""
}
onClick=
{
()
=>
setEnvironment
(
"preview"
)
}
><
Monitor
size=
{
13
}
/>
测试预览
</
button
><
button
className=
{
showingProduction
?
"active"
:
""
}
onClick=
{
()
=>
setEnvironment
(
"production"
)
}
><
Globe2
size=
{
13
}
/>
生产站点
</
button
></
div
>
<
div
className=
"device-toggle"
><
button
className=
{
device
===
"desktop"
?
"active"
:
""
}
onClick=
{
()
=>
setDevice
(
"desktop"
)
}
><
Laptop
size=
{
15
}
/></
button
><
button
className=
{
device
===
"mobile"
?
"active"
:
""
}
onClick=
{
()
=>
setDevice
(
"mobile"
)
}
><
Smartphone
size=
{
15
}
/></
button
></
div
>
<
div
className=
"device-toggle"
><
button
className=
{
device
===
"desktop"
?
"active"
:
""
}
onClick=
{
()
=>
setDevice
(
"desktop"
)
}
><
Laptop
size=
{
15
}
/></
button
><
button
className=
{
device
===
"mobile"
?
"active"
:
""
}
onClick=
{
()
=>
setDevice
(
"mobile"
)
}
><
Smartphone
size=
{
15
}
/></
button
></
div
>
...
@@ -310,19 +395,17 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -310,19 +395,17 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
</
button
>
}
</
button
>
}
</
div
>
</
div
>
</
header
>
</
header
>
<section className="version-state-strip">
<article><span>当前正在看</span><strong>{viewingLabel}</strong><small>{showingProduction ? "生产链接" : historicalPreview ? "只读测试预览" : "测试链接"}</small></article>
<article className={site.lastError ? "error" : ""}><span>最近保存 <em>{savedState}</em></span><strong>{site.currentCommit.slice(0, 7)}</strong><small>{site.lastError ? "修改失败,已保留原测试版本" : "最近一次成功保存的可编辑版本"}</small></article>
<article className="production"><span>线上运行</span><strong>{site.publishedCommit?.slice(0, 7) || "尚未上线"}</strong><small>{site.lastPublishError ? "上线失败,原线上版本保持不变" : site.publishedAt ? formatDate(site.publishedAt) : "生产链接已保留"}</small></article>
</section>
<
div
className=
"browser-stage"
>
<
div
className=
"browser-stage"
>
<
div
className=
{
`browser-frame ${device}`
}
>
<
div
className=
{
`browser-frame ${device}`
}
>
<
div
className=
"browser-chrome"
><
div
><
i
/><
i
/><
i
/></
div
><
span
>
{
activeUrl
||
site
.
productionUrl
||
"/sites/"
+
site
.
siteId
+
"/"
}
</
span
><
RefreshCw
size=
{
11
}
/></
div
>
<
div
className=
"browser-chrome"
><
div
><
i
/><
i
/><
i
/></
div
><
span
>
{
activeUrl
||
site
.
productionUrl
||
"/sites/"
+
site
.
siteId
+
"/"
}
</
span
><
RefreshCw
size=
{
11
}
/></
div
>
<
iframe
key=
{
environment
+
iframeVersion
}
title=
{
site
.
name
+
(
showingProduction
?
" 生产站点"
:
" 测试预览"
)
}
src=
{
activeUrl
+
"?v="
+
iframeVersion
}
/>
<
iframe
key=
{
environment
+
iframeVersion
}
title=
{
site
.
name
+
(
showingProduction
?
" 生产站点"
:
" 测试预览"
)
}
src=
{
activeUrl
+
"?v="
+
iframeVersion
}
/>
{
busy && <div className="preview-busy"><LoaderCircle className="spin" size={25} /><strong>{publishMutation.isPending ? "正在发布生产版本
" : "正在生成测试版本"}</strong><span>{publishMutation.isPending ? "测试环境和原生产版本保持可用" : "当前测试预览保持可用"}</span></div>}
{
(
publishMutation
.
isPending
||
rebuildMutation
.
isPending
||
previewVersionMutation
.
isPending
||
previewDraftMutation
.
isPending
||
undoMutation
.
isPending
)
&&
<
div
className=
"preview-busy"
><
LoaderCircle
className=
"spin"
size=
{
25
}
/><
strong
>
{
publishMutation
.
isPending
?
"正在发布生产版本"
:
previewDraftMutation
.
isPending
?
"正在构建工作草稿
"
:
"正在生成测试版本"
}
</
strong
><
span
>
{
publishMutation
.
isPending
?
"测试环境和原生产版本保持可用"
:
"当前测试预览保持可用"
}
</
span
></
div
>
}
</
div
>
</
div
>
</
div
>
</
div
>
<footer className="preview-footer"><span><Clock3 size={12} /> 测试版本 {previewVersion?.shortHash || site.currentCommit.slice(0, 7)} · {previewVersion ? formatDate(previewVersion.date) : formatDate(site.updatedAt)}</span><span><Palette size={12} />{site.publishedAt ? `
生产版本
$
{
productionVersion
?.
shortHash
||
site
.
publishedCommit
?.
slice
(
0
,
7
)}
·
$
{
formatDate
(
site
.
publishedAt
)}
` : "生产环境尚未发布"}</span></footer>
<
footer
className=
"preview-footer"
>
<
span
>
{
showingProduction
?
<
Globe2
size=
{
12
}
/>
:
<
Monitor
size=
{
12
}
/>
}
当前查看 ·
{
showingProduction
?
`生产版本 ${productionVersion?.shortHash || site.publishedCommit?.slice(0, 7) || "尚未上线"}`
:
`${historicalPreview ? "历史版本" : "测试版本"} ${previewVersion?.shortHash || site.currentCommit.slice(0, 7)}`
}
·
{
showingProduction
?
site
.
publishedAt
?
formatDate
(
site
.
publishedAt
)
:
"尚未发布"
:
previewVersion
?
formatDate
(
previewVersion
.
date
)
:
formatDate
(
site
.
updatedAt
)
}{
historicalPreview
&&
!
showingProduction
?
" · 只读"
:
""
}
</
span
>
<
span
><
Clock3
size=
{
12
}
/>
{
site
.
publishedAt
?
`线上版本 ${productionVersion?.shortHash || site.publishedCommit?.slice(0, 7)} · ${formatDate(site.publishedAt)}`
:
"生产环境尚未发布"
}
</
span
>
</
footer
>
</
section
>
</
section
>
</
main
>;
</
main
>;
}
}
...
@@ -330,3 +413,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -330,3 +413,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
function
formatDate
(
value
:
string
)
{
function
formatDate
(
value
:
string
)
{
return
new
Intl
.
DateTimeFormat
(
"zh-CN"
,
{
month
:
"short"
,
day
:
"numeric"
,
hour
:
"2-digit"
,
minute
:
"2-digit"
}).
format
(
new
Date
(
value
));
return
new
Intl
.
DateTimeFormat
(
"zh-CN"
,
{
month
:
"short"
,
day
:
"numeric"
,
hour
:
"2-digit"
,
minute
:
"2-digit"
}).
format
(
new
Date
(
value
));
}
}
function
formatTime
(
value
:
string
)
{
return
new
Intl
.
DateTimeFormat
(
"zh-CN"
,
{
hour
:
"2-digit"
,
minute
:
"2-digit"
}).
format
(
new
Date
(
value
));
}
frontend/src/api.ts
View file @
f86dd1a6
...
@@ -2,6 +2,8 @@ import type { ChatResult, CreateSiteInput, GitHistoryItem, PreviewVersionResult,
...
@@ -2,6 +2,8 @@ import type { ChatResult, CreateSiteInput, GitHistoryItem, PreviewVersionResult,
async
function
request
<
T
>
(
url
:
string
,
options
?:
RequestInit
):
Promise
<
T
>
{
async
function
request
<
T
>
(
url
:
string
,
options
?:
RequestInit
):
Promise
<
T
>
{
const
headers
=
new
Headers
(
options
?.
headers
);
const
headers
=
new
Headers
(
options
?.
headers
);
const
token
=
sessionStorage
.
getItem
(
"webagent-session"
);
if
(
token
)
headers
.
set
(
"authorization"
,
"Bearer "
+
token
);
if
(
options
?.
body
!=
null
&&
!
headers
.
has
(
"content-type"
))
headers
.
set
(
"content-type"
,
"application/json"
);
if
(
options
?.
body
!=
null
&&
!
headers
.
has
(
"content-type"
))
headers
.
set
(
"content-type"
,
"application/json"
);
const
response
=
await
fetch
(
url
,
{
const
response
=
await
fetch
(
url
,
{
...
options
,
...
options
,
...
@@ -13,6 +15,9 @@ async function request<T>(url: string, options?: RequestInit): Promise<T> {
...
@@ -13,6 +15,9 @@ async function request<T>(url: string, options?: RequestInit): Promise<T> {
}
}
export
const
api
=
{
export
const
api
=
{
login
:
(
username
:
string
,
password
:
string
)
=>
request
<
{
token
:
string
;
username
:
string
}
>
(
"/api/login"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
username
,
password
})
}),
logout
:
()
=>
request
<
{
success
:
true
}
>
(
"/api/logout"
,
{
method
:
"POST"
}),
session
:
()
=>
request
<
{
username
:
string
}
>
(
"/api/session"
),
health
:
()
=>
request
<
{
status
:
string
;
agentMode
:
"model"
|
"local"
}
>
(
"/api/health"
),
health
:
()
=>
request
<
{
status
:
string
;
agentMode
:
"model"
|
"local"
}
>
(
"/api/health"
),
sites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/sites"
),
sites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/sites"
),
site
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/sites/"
+
siteId
),
site
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/sites/"
+
siteId
),
...
@@ -20,6 +25,7 @@ export const api = {
...
@@ -20,6 +25,7 @@ export const api = {
chat
:
(
siteId
:
string
,
message
:
string
)
=>
request
<
ChatResult
>
(
"/api/sites/"
+
siteId
+
"/chat"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
message
})
}),
chat
:
(
siteId
:
string
,
message
:
string
)
=>
request
<
ChatResult
>
(
"/api/sites/"
+
siteId
+
"/chat"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
message
})
}),
history
:
(
siteId
:
string
)
=>
request
<
GitHistoryItem
[]
>
(
"/api/sites/"
+
siteId
+
"/history"
),
history
:
(
siteId
:
string
)
=>
request
<
GitHistoryItem
[]
>
(
"/api/sites/"
+
siteId
+
"/history"
),
previewVersion
:
(
siteId
:
string
,
commit
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-version"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
commit
})
}),
previewVersion
:
(
siteId
:
string
,
commit
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-version"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
commit
})
}),
previewDraft
:
(
siteId
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-draft"
,
{
method
:
"POST"
}),
rebuild
:
(
siteId
:
string
)
=>
request
<
{
previewUrl
:
string
}
>
(
"/api/sites/"
+
siteId
+
"/build"
,
{
method
:
"POST"
}),
rebuild
:
(
siteId
:
string
)
=>
request
<
{
previewUrl
:
string
}
>
(
"/api/sites/"
+
siteId
+
"/build"
,
{
method
:
"POST"
}),
publish
:
(
siteId
:
string
)
=>
request
<
PublishResult
>
(
"/api/sites/"
+
siteId
+
"/publish"
,
{
method
:
"POST"
}),
publish
:
(
siteId
:
string
)
=>
request
<
PublishResult
>
(
"/api/sites/"
+
siteId
+
"/publish"
,
{
method
:
"POST"
}),
undo
:
(
siteId
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/undo"
,
{
method
:
"POST"
}),
undo
:
(
siteId
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/undo"
,
{
method
:
"POST"
}),
...
...
frontend/src/styles.css
View file @
f86dd1a6
...
@@ -13,15 +13,19 @@
...
@@ -13,15 +13,19 @@
.landing-header
{
position
:
relative
;
z-index
:
2
;
width
:
min
(
1380px
,
calc
(
100%
-
64px
));
height
:
88px
;
margin
:
auto
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
}
.landing-status
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
8px
12px
;
color
:
var
(
--text-2
);
background
:
rgba
(
255
,
255
,
255
,
.72
);
border
:
1px
solid
rgba
(
231
,
227
,
243
,
.9
);
border-radius
:
999px
;
font-size
:
11px
;
font-weight
:
650
;
backdrop-filter
:
blur
(
14px
)}
.landing-status
>
span
:first-child
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#20b486
;
box-shadow
:
0
0
0
4px
rgba
(
32
,
180
,
134
,
.12
)}
.landing-header
{
position
:
relative
;
z-index
:
2
;
width
:
min
(
1380px
,
calc
(
100%
-
64px
));
height
:
88px
;
margin
:
auto
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
}
.landing-status
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
8px
12px
;
color
:
var
(
--text-2
);
background
:
rgba
(
255
,
255
,
255
,
.72
);
border
:
1px
solid
rgba
(
231
,
227
,
243
,
.9
);
border-radius
:
999px
;
font-size
:
11px
;
font-weight
:
650
;
backdrop-filter
:
blur
(
14px
)}
.landing-status
>
span
:first-child
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#20b486
;
box-shadow
:
0
0
0
4px
rgba
(
32
,
180
,
134
,
.12
)}
.create-layout
{
position
:
relative
;
z-index
:
1
;
width
:
min
(
1320px
,
calc
(
100%
-
64px
));
min-height
:
calc
(
100vh
-
88px
);
margin
:
auto
;
display
:
grid
;
grid-template-columns
:
.92
fr
1.08
fr
;
gap
:
85px
;
align-items
:
center
;
padding
:
42px
0
72px
}
.create-intro
{
padding-left
:
8px
}
.back-button
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin-bottom
:
38px
;
padding
:
0
;
color
:
var
(
--text-2
);
border
:
0
;
background
:
none
;
font-size
:
12px
}
.intro-tag
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
8px
12px
;
color
:
var
(
--primary
);
background
:
rgba
(
255
,
255
,
255
,
.8
);
border
:
1px
solid
#ded6ff
;
border-radius
:
999px
;
font-size
:
10px
;
font-weight
:
850
;
letter-spacing
:
.13em
}
.create-intro
h1
{
max-width
:
620px
;
margin
:
25px
0
24px
;
font-size
:
clamp
(
51px
,
5.3vw
,
78px
);
line-height
:
1.07
;
letter-spacing
:
-.065em
}
.create-intro
h1
span
{
display
:
inline-block
;
color
:
transparent
;
background
:
var
(
--gradient
);
background-clip
:
text
}
.create-intro
>
p
{
max-width
:
570px
;
margin
:
0
;
color
:
var
(
--text-2
);
font-size
:
17px
;
line-height
:
1.85
}
.feature-row
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
12px
;
margin-top
:
52px
}
.feature-row
>
div
{
padding
:
17px
13px
;
background
:
rgba
(
255
,
255
,
255
,
.62
);
border
:
1px
solid
rgba
(
231
,
227
,
243
,
.9
);
border-radius
:
16px
}
.feature-row
div
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
35px
;
height
:
35px
;
margin-bottom
:
13px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
10px
}
.feature-row
strong
,
.feature-row
small
{
display
:
block
}
.feature-row
strong
{
font-size
:
12px
;
margin-bottom
:
6px
}
.feature-row
small
{
color
:
var
(
--muted
);
font-size
:
10px
;
line-height
:
1.5
}
.create-layout
{
position
:
relative
;
z-index
:
1
;
width
:
min
(
1320px
,
calc
(
100%
-
64px
));
min-height
:
calc
(
100vh
-
88px
);
margin
:
auto
;
display
:
grid
;
grid-template-columns
:
.92
fr
1.08
fr
;
gap
:
85px
;
align-items
:
center
;
padding
:
42px
0
72px
}
.create-intro
{
padding-left
:
8px
}
.back-button
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin-bottom
:
38px
;
padding
:
0
;
color
:
var
(
--text-2
);
border
:
0
;
background
:
none
;
font-size
:
12px
}
.intro-tag
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
8px
12px
;
color
:
var
(
--primary
);
background
:
rgba
(
255
,
255
,
255
,
.8
);
border
:
1px
solid
#ded6ff
;
border-radius
:
999px
;
font-size
:
10px
;
font-weight
:
850
;
letter-spacing
:
.13em
}
.create-intro
h1
{
max-width
:
620px
;
margin
:
25px
0
24px
;
font-size
:
clamp
(
51px
,
5.3vw
,
78px
);
line-height
:
1.07
;
letter-spacing
:
-.065em
}
.create-intro
h1
span
{
display
:
inline-block
;
color
:
transparent
;
background
:
var
(
--gradient
);
background-clip
:
text
}
.create-intro
>
p
{
max-width
:
570px
;
margin
:
0
;
color
:
var
(
--text-2
);
font-size
:
17px
;
line-height
:
1.85
}
.feature-row
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
12px
;
margin-top
:
52px
}
.feature-row
>
div
{
padding
:
17px
13px
;
background
:
rgba
(
255
,
255
,
255
,
.62
);
border
:
1px
solid
rgba
(
231
,
227
,
243
,
.9
);
border-radius
:
16px
}
.feature-row
div
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
35px
;
height
:
35px
;
margin-bottom
:
13px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
10px
}
.feature-row
strong
,
.feature-row
small
{
display
:
block
}
.feature-row
strong
{
font-size
:
12px
;
margin-bottom
:
6px
}
.feature-row
small
{
color
:
var
(
--muted
);
font-size
:
10px
;
line-height
:
1.5
}
.create-card-wrap
{
display
:
flex
;
justify-content
:
flex-end
}
.create-card
{
width
:
min
(
100%
,
650px
);
padding
:
35px
38px
25px
;
background
:
rgba
(
255
,
255
,
255
,
.91
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
.95
);
border-radius
:
26px
;
box-shadow
:
0
32px
90px
rgba
(
36
,
16
,
95
,
.13
),
0
2px
6px
rgba
(
36
,
16
,
95
,
.04
);
backdrop-filter
:
blur
(
24px
)}
.card-heading
{
display
:
flex
;
align-items
:
flex-start
;
justify-content
:
space-between
;
margin-bottom
:
27px
}
.card-heading
span
{
color
:
var
(
--primary
);
font-size
:
10px
;
font-weight
:
800
;
letter-spacing
:
.12em
}
.card-heading
h2
{
margin
:
7px
0
0
;
font-size
:
24px
;
letter-spacing
:
-.035em
}
.step-pill
{
padding
:
7px
10px
;
color
:
var
(
--muted
);
background
:
#f8f8fc
;
border-radius
:
9px
;
font-size
:
10px
;
font-weight
:
700
}
.form-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
17px
}
.field
{
display
:
flex
;
flex-direction
:
column
;
gap
:
8px
}
.field.wide
{
grid-column
:
1
/
-1
}
.field
>
span
{
display
:
flex
;
justify-content
:
space-between
;
color
:
var
(
--text-2
);
font-size
:
11px
;
font-weight
:
700
}
.field
small
{
color
:
var
(
--muted
);
font-weight
:
500
}
.field
input
,
.field
textarea
{
width
:
100%
;
padding
:
11px
13px
;
color
:
var
(
--text
);
background
:
#fbfbfd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
10px
;
outline
:
none
;
font-size
:
13px
;
transition
:
.2s
}
.field
textarea
{
resize
:
vertical
;
line-height
:
1.6
}
.field
input
:focus
,
.field
textarea
:focus
{
background
:
#fff
;
border-color
:
#ad8cff
;
box-shadow
:
0
0
0
3px
rgba
(
112
,
40
,
255
,
.08
)}
.color-field
{
height
:
44px
;
display
:
flex
;
align-items
:
center
;
gap
:
11px
;
padding
:
5px
8px
;
background
:
#fbfbfd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
10px
}
.color-field
>
input
{
width
:
31px
;
height
:
31px
;
padding
:
2px
;
border
:
0
;
background
:
none
}
.color-field
>
span
{
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
color
:
var
(
--text-2
);
font-size
:
11px
}
.color-swatches
{
display
:
flex
;
gap
:
7px
;
margin-left
:
auto
}
.color-swatches
button
{
width
:
21px
;
height
:
21px
;
padding
:
0
;
border
:
2px
solid
white
;
border-radius
:
7px
;
box-shadow
:
0
0
0
1px
var
(
--border
)}
.color-swatches
button
.active
{
box-shadow
:
0
0
0
2px
var
(
--primary
)}
.create-submit
{
width
:
100%
;
height
:
51px
;
margin-top
:
24px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
11px
;
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
;
border-radius
:
12px
;
box-shadow
:
0
13px
28px
rgba
(
112
,
40
,
255
,
.24
);
font-size
:
13px
;
font-weight
:
800
;
transition
:
.2s
}
.create-submit
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-1px
);
box-shadow
:
0
16px
34px
rgba
(
112
,
40
,
255
,
.32
)}
.local-note
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
6px
;
margin
:
14px
0
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.form-error
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin-top
:
16px
;
padding
:
10px
12px
;
color
:
#b42318
;
background
:
#fff2f0
;
border-radius
:
9px
;
font-size
:
11px
}
.create-card-wrap
{
display
:
flex
;
justify-content
:
flex-end
}
.create-card
{
width
:
min
(
100%
,
650px
);
padding
:
35px
38px
25px
;
background
:
rgba
(
255
,
255
,
255
,
.91
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
.95
);
border-radius
:
26px
;
box-shadow
:
0
32px
90px
rgba
(
36
,
16
,
95
,
.13
),
0
2px
6px
rgba
(
36
,
16
,
95
,
.04
);
backdrop-filter
:
blur
(
24px
)}
.card-heading
{
display
:
flex
;
align-items
:
flex-start
;
justify-content
:
space-between
;
margin-bottom
:
27px
}
.card-heading
span
{
color
:
var
(
--primary
);
font-size
:
10px
;
font-weight
:
800
;
letter-spacing
:
.12em
}
.card-heading
h2
{
margin
:
7px
0
0
;
font-size
:
24px
;
letter-spacing
:
-.035em
}
.step-pill
{
padding
:
7px
10px
;
color
:
var
(
--muted
);
background
:
#f8f8fc
;
border-radius
:
9px
;
font-size
:
10px
;
font-weight
:
700
}
.form-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
17px
}
.field
{
display
:
flex
;
flex-direction
:
column
;
gap
:
8px
}
.field.wide
{
grid-column
:
1
/
-1
}
.field
>
span
{
display
:
flex
;
justify-content
:
space-between
;
color
:
var
(
--text-2
);
font-size
:
11px
;
font-weight
:
700
}
.field
small
{
color
:
var
(
--muted
);
font-weight
:
500
}
.field
input
,
.field
textarea
{
width
:
100%
;
padding
:
11px
13px
;
color
:
var
(
--text
);
background
:
#fbfbfd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
10px
;
outline
:
none
;
font-size
:
13px
;
transition
:
.2s
}
.field
textarea
{
resize
:
vertical
;
line-height
:
1.6
}
.field
input
:focus
,
.field
textarea
:focus
{
background
:
#fff
;
border-color
:
#ad8cff
;
box-shadow
:
0
0
0
3px
rgba
(
112
,
40
,
255
,
.08
)}
.color-field
{
height
:
44px
;
display
:
flex
;
align-items
:
center
;
gap
:
11px
;
padding
:
5px
8px
;
background
:
#fbfbfd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
10px
}
.color-field
>
input
{
width
:
31px
;
height
:
31px
;
padding
:
2px
;
border
:
0
;
background
:
none
}
.color-field
>
span
{
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
color
:
var
(
--text-2
);
font-size
:
11px
}
.color-swatches
{
display
:
flex
;
gap
:
7px
;
margin-left
:
auto
}
.color-swatches
button
{
width
:
21px
;
height
:
21px
;
padding
:
0
;
border
:
2px
solid
white
;
border-radius
:
7px
;
box-shadow
:
0
0
0
1px
var
(
--border
)}
.color-swatches
button
.active
{
box-shadow
:
0
0
0
2px
var
(
--primary
)}
.create-submit
{
width
:
100%
;
height
:
51px
;
margin-top
:
24px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
11px
;
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
;
border-radius
:
12px
;
box-shadow
:
0
13px
28px
rgba
(
112
,
40
,
255
,
.24
);
font-size
:
13px
;
font-weight
:
800
;
transition
:
.2s
}
.create-submit
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-1px
);
box-shadow
:
0
16px
34px
rgba
(
112
,
40
,
255
,
.32
)}
.local-note
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
6px
;
margin
:
14px
0
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.form-error
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin-top
:
16px
;
padding
:
10px
12px
;
color
:
#b42318
;
background
:
#fff2f0
;
border-radius
:
9px
;
font-size
:
11px
}
.workspace
{
height
:
100vh
;
height
:
100
dvh
;
max-height
:
100
dvh
;
min-height
:
0
;
display
:
grid
;
grid-template-columns
:
64px
372px
1
fr
;
overflow
:
hidden
;
background
:
#eff0f5
}
.rail
{
min-height
:
0
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
padding
:
15px
0
;
background
:
#fff
;
border-right
:
1px
solid
var
(
--divider
);
z-index
:
3
}
.rail
.logo-mark
{
width
:
35px
;
height
:
35px
}
.rail-nav
{
display
:
flex
;
flex-direction
:
column
;
gap
:
9px
;
margin-top
:
42px
}
.rail
button
{
display
:
grid
;
place-items
:
center
;
width
:
38px
;
height
:
38px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
11px
}
.rail
button
:hover
,
.rail
button
.active
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.rail-create
{
margin-top
:
auto
!important
;
color
:
white
!important
;
background
:
var
(
--gradient
)
!important
;
box-shadow
:
0
8px
20px
rgba
(
112
,
40
,
255
,
.2
)}
.login-layout
{
grid-template-columns
:
1.05
fr
.95
fr
}
.login-card-wrap
{
align-items
:
center
}
.login-card
{
width
:
min
(
100%
,
480px
);
padding
:
42px
42px
28px
}
.login-card-icon
{
display
:
grid
;
place-items
:
center
;
width
:
45px
;
height
:
45px
;
margin-bottom
:
22px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border
:
1px
solid
#ded6ff
;
border-radius
:
14px
}
.login-card
.card-heading
{
margin-bottom
:
10px
}
.login-copy
{
margin
:
0
0
26px
;
color
:
var
(
--muted
);
font-size
:
12px
;
line-height
:
1.7
}
.login-fields
{
display
:
flex
;
flex-direction
:
column
;
gap
:
18px
}
.login-submit
{
margin-top
:
26px
}
.create-workspace
{
grid-template-columns
:
64px
320px
1
fr
}
.rail-bottom
{
margin-top
:
auto
;
display
:
flex
;
flex-direction
:
column
;
gap
:
8px
}
.rail
.rail-bottom
.rail-create
{
margin-top
:
0
!important
}
.rail
.rail-logout
{
color
:
#9a9da8
}
.rail
.rail-logout
:hover
{
color
:
#b42318
;
background
:
#fff0ee
}
.create-workspace
>
.rail
>
.rail-logout
{
margin-top
:
auto
}
.create-control-panel
{
background
:
linear-gradient
(
180deg
,
#fff
,
#faf9ff
)}
.empty-site-heading
{
height
:
86px
;
display
:
flex
;
align-items
:
flex-start
;
justify-content
:
center
;
flex-direction
:
column
;
gap
:
5px
;
padding
:
16px
20px
;
border-bottom
:
1px
solid
var
(
--divider
)}
.empty-site-heading
.logo
{
font-size
:
15px
}
.empty-site-heading
.logo-mark
{
width
:
31px
;
height
:
31px
}
.empty-site-heading
small
{
padding-left
:
42px
;
color
:
var
(
--muted
);
font-size
:
8px
}
.create-side-copy
{
padding
:
42px
28px
}
.create-side-copy
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
38px
;
height
:
38px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
12px
}
.create-side-copy
h2
{
margin
:
18px
0
9px
;
font-size
:
20px
}
.create-side-copy
p
{
margin
:
0
;
color
:
var
(
--muted
);
font-size
:
11px
;
line-height
:
1.8
}
.create-side-copy
ul
{
margin
:
25px
0
0
;
padding
:
0
;
list-style
:
none
;
color
:
var
(
--text-2
);
font-size
:
10px
}
.create-side-copy
li
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
margin-top
:
12px
}
.create-side-copy
li
::before
{
content
:
""
;
width
:
6px
;
height
:
6px
;
background
:
var
(
--primary
);
border-radius
:
50%
;
box-shadow
:
0
0
0
4px
rgba
(
112
,
40
,
255
,
.08
)}
.workspace-create-content
{
min-width
:
0
;
min-height
:
0
;
overflow
:
auto
;
display
:
flex
;
align-items
:
flex-start
;
justify-content
:
center
;
padding
:
42px
;
background
:
linear-gradient
(
145deg
,
#f4f1ff
,
#f8f8fc
)}
.workspace-create-card
{
width
:
min
(
780px
,
100%
);
border
:
1px
solid
#fff
;
border-radius
:
20px
}
.workspace-create-card
.card-heading
p
{
margin
:
7px
0
0
;
color
:
var
(
--muted
);
font-size
:
10px
}
.workspace-create-cancel
{
height
:
31px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
padding
:
0
10px
;
color
:
var
(
--muted
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
font-size
:
9px
;
font-weight
:
700
}
.workspace-create-cancel
:hover
{
color
:
#b42318
;
background
:
#fff5f3
}
.workspace
{
height
:
100vh
;
height
:
100
dvh
;
max-height
:
100
dvh
;
min-height
:
0
;
display
:
grid
;
grid-template-columns
:
64px
372px
1
fr
;
overflow
:
hidden
;
background
:
#eff0f5
}
.rail
{
min-height
:
0
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
padding
:
15px
0
;
background
:
#fff
;
border-right
:
1px
solid
var
(
--divider
);
z-index
:
3
}
.rail
.logo-mark
{
width
:
35px
;
height
:
35px
}
.rail-nav
{
display
:
flex
;
flex-direction
:
column
;
gap
:
9px
;
margin-top
:
16px
}
.rail
button
{
display
:
grid
;
place-items
:
center
;
width
:
38px
;
height
:
38px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
11px
}
.rail
button
:hover
,
.rail
button
.active
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.rail
.rail-create
{
color
:
white
!important
;
background
:
var
(
--gradient
)
!important
;
box-shadow
:
0
8px
20px
rgba
(
112
,
40
,
255
,
.2
)}
.rail-create-top
{
flex
:
0
0
auto
;
margin-top
:
13px
!important
}
.rail-create-top.active
{
box-shadow
:
0
0
0
3px
rgba
(
112
,
40
,
255
,
.13
),
0
8px
20px
rgba
(
112
,
40
,
255
,
.2
)
!important
}
.rail-settings
{
position
:
relative
;
margin-top
:
auto
}
.rail-settings.open
.rail-settings-trigger
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.rail-settings-menu
{
position
:
absolute
;
left
:
47px
;
bottom
:
0
;
width
:
178px
;
padding
:
8px
;
background
:
rgba
(
255
,
255
,
255
,
.98
);
border
:
1px
solid
#dfd9ef
;
border-radius
:
13px
;
box-shadow
:
0
18px
48px
rgba
(
36
,
16
,
95
,
.18
);
backdrop-filter
:
blur
(
16px
)}
.rail-settings-menu
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
padding
:
7px
7px
10px
;
border-bottom
:
1px
solid
var
(
--divider
)}
.settings-avatar
{
display
:
grid
;
place-items
:
center
;
width
:
30px
;
height
:
30px
;
flex
:
0
0
auto
;
color
:
white
;
background
:
var
(
--gradient
);
border-radius
:
9px
;
font-size
:
11px
;
font-weight
:
800
}
.rail-settings-menu
>
div
>
span
:last-child
{
min-width
:
0
;
display
:
flex
;
flex-direction
:
column
;
gap
:
3px
}
.rail-settings-menu
strong
{
font-size
:
10px
}
.rail-settings-menu
small
{
color
:
var
(
--muted
);
font-size
:
8px
}
.rail
.rail-settings-menu
>
button
{
width
:
100%
;
height
:
33px
;
margin-top
:
5px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
flex-start
;
gap
:
7px
;
padding
:
0
8px
;
color
:
#b42318
;
border-radius
:
8px
;
font-size
:
9px
;
font-weight
:
700
}
.rail
.rail-settings-menu
>
button
:hover
{
background
:
#fff0ee
}
.control-panel
{
height
:
100%
;
min-height
:
0
;
min-width
:
0
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
;
background
:
#fff
;
border-right
:
1px
solid
var
(
--border
);
z-index
:
2
}
.site-switcher
{
position
:
relative
;
z-index
:
20
;
flex
:
0
0
66px
;
height
:
66px
;
padding
:
8px
12px
;
border-bottom
:
1px
solid
var
(
--divider
)}
.site-switcher-trigger
{
width
:
100%
;
height
:
49px
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
5px
7px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
#fff
;
border
:
1px
solid
transparent
;
border-radius
:
12px
;
transition
:
.18s
ease
}
.site-switcher-trigger
:hover
,
.site-switcher.open
.site-switcher-trigger
{
background
:
#faf9ff
;
border-color
:
#ded6ff
;
box-shadow
:
0
5px
18px
rgba
(
36
,
16
,
95
,
.06
)}
.site-avatar
,
.site-menu-avatar
{
display
:
grid
;
place-items
:
center
;
flex
:
0
0
auto
;
color
:
white
;
background
:
var
(
--gradient
);
font-weight
:
800
}
.site-avatar
{
width
:
35px
;
height
:
35px
;
border-radius
:
10px
;
font-size
:
12px
;
box-shadow
:
0
7px
16px
rgba
(
112
,
40
,
255
,
.18
)}
.site-switcher-copy
,
.site-menu-copy
{
min-width
:
0
;
display
:
flex
;
flex-direction
:
column
}
.site-switcher-copy
{
flex
:
1
;
gap
:
3px
}
.site-switcher-copy
strong
{
overflow
:
hidden
;
font-size
:
12px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-switcher-copy
small
{
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-switcher-chevron
{
display
:
grid
;
place-items
:
center
;
width
:
26px
;
height
:
26px
;
color
:
var
(
--muted
);
background
:
#f4f4f7
;
border-radius
:
8px
;
transition
:
.2s
ease
}
.site-switcher.open
.site-switcher-chevron
{
color
:
var
(
--primary
);
background
:
var
(
--soft
);
transform
:
rotate
(
180deg
)}
.site-switcher-menu
{
position
:
absolute
;
top
:
61px
;
left
:
12px
;
right
:
12px
;
padding
:
8px
;
background
:
rgba
(
255
,
255
,
255
,
.98
);
border
:
1px
solid
#dfd9ef
;
border-radius
:
14px
;
box-shadow
:
0
20px
52px
rgba
(
36
,
16
,
95
,
.18
),
0
4px
12px
rgba
(
36
,
16
,
95
,
.07
);
backdrop-filter
:
blur
(
18px
);
animation
:
site-menu-in
.16s
ease-out
}
@keyframes
site-menu-in
{
from
{
opacity
:
0
;
transform
:
translateY
(
-5px
)
scale
(
.985
)}
to
{
opacity
:
1
;
transform
:
none
}}
.site-menu-heading
{
height
:
28px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
padding
:
0
7px
;
color
:
var
(
--muted
);
font-size
:
8px
;
font-weight
:
750
;
letter-spacing
:
.04em
}
.site-menu-heading
em
{
display
:
grid
;
place-items
:
center
;
width
:
19px
;
height
:
19px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
6px
;
font-size
:
8px
;
font-style
:
normal
}
.site-menu-list
{
display
:
flex
;
flex-direction
:
column
;
gap
:
3px
}
.site-menu-list
>
button
{
width
:
100%
;
min-width
:
0
;
height
:
52px
;
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
padding
:
6px
8px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
transparent
;
border
:
0
;
border-radius
:
10px
}
.site-menu-list
>
button
:hover
{
background
:
#f7f5ff
}
.site-menu-list
>
button
.selected
{
background
:
#f4f0ff
}
.site-menu-avatar
{
width
:
32px
;
height
:
32px
;
border-radius
:
9px
;
font-size
:
11px
}
.site-menu-copy
{
flex
:
1
;
gap
:
4px
}
.site-menu-copy
strong
{
overflow
:
hidden
;
font-size
:
10px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-menu-copy
small
{
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-menu-status
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#d0d1d8
;
box-shadow
:
0
0
0
3px
rgba
(
208
,
209
,
216
,
.15
)}
.site-menu-status.ready
{
background
:
#7957d5
;
box-shadow
:
0
0
0
3px
rgba
(
121
,
87
,
213
,
.1
)}
.site-menu-status.published
{
background
:
#20b486
;
box-shadow
:
0
0
0
3px
rgba
(
32
,
180
,
134
,
.1
)}
.site-menu-status.failed
{
background
:
#d92d20
;
box-shadow
:
0
0
0
3px
rgba
(
217
,
45
,
32
,
.1
)}
.site-menu-list
>
button
>
svg
{
flex
:
0
0
auto
;
color
:
var
(
--primary
)}
.site-switcher-menu
>
p
{
height
:
27px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin
:
6px
3px
0
;
padding
:
6px
5px
0
;
color
:
var
(
--muted
);
border-top
:
1px
solid
var
(
--divider
);
font-size
:
7px
}
.panel-tabs
{
flex
:
0
0
49px
;
height
:
49px
;
padding
:
0
14px
;
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
5px
;
align-items
:
center
;
border-bottom
:
1px
solid
var
(
--divider
)}
.panel-tabs
button
{
height
:
35px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
7px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
9px
;
font-size
:
11px
;
font-weight
:
700
}
.panel-tabs
button
.active
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.control-panel
{
height
:
100%
;
min-height
:
0
;
min-width
:
0
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
;
background
:
#fff
;
border-right
:
1px
solid
var
(
--border
);
z-index
:
2
}
.site-switcher
{
position
:
relative
;
z-index
:
20
;
flex
:
0
0
66px
;
height
:
66px
;
padding
:
8px
12px
;
border-bottom
:
1px
solid
var
(
--divider
)}
.site-switcher-trigger
{
width
:
100%
;
height
:
49px
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
5px
7px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
#fff
;
border
:
1px
solid
transparent
;
border-radius
:
12px
;
transition
:
.18s
ease
}
.site-switcher-trigger
:hover
,
.site-switcher.open
.site-switcher-trigger
{
background
:
#faf9ff
;
border-color
:
#ded6ff
;
box-shadow
:
0
5px
18px
rgba
(
36
,
16
,
95
,
.06
)}
.site-avatar
,
.site-menu-avatar
{
display
:
grid
;
place-items
:
center
;
flex
:
0
0
auto
;
color
:
white
;
background
:
var
(
--gradient
);
font-weight
:
800
}
.site-avatar
{
width
:
35px
;
height
:
35px
;
border-radius
:
10px
;
font-size
:
12px
;
box-shadow
:
0
7px
16px
rgba
(
112
,
40
,
255
,
.18
)}
.site-switcher-copy
,
.site-menu-copy
{
min-width
:
0
;
display
:
flex
;
flex-direction
:
column
}
.site-switcher-copy
{
flex
:
1
;
gap
:
3px
}
.site-switcher-copy
strong
{
overflow
:
hidden
;
font-size
:
12px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-switcher-copy
small
{
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-switcher-chevron
{
display
:
grid
;
place-items
:
center
;
width
:
26px
;
height
:
26px
;
color
:
var
(
--muted
);
background
:
#f4f4f7
;
border-radius
:
8px
;
transition
:
.2s
ease
}
.site-switcher.open
.site-switcher-chevron
{
color
:
var
(
--primary
);
background
:
var
(
--soft
);
transform
:
rotate
(
180deg
)}
.site-switcher-menu
{
position
:
absolute
;
top
:
61px
;
left
:
12px
;
right
:
12px
;
padding
:
8px
;
background
:
rgba
(
255
,
255
,
255
,
.98
);
border
:
1px
solid
#dfd9ef
;
border-radius
:
14px
;
box-shadow
:
0
20px
52px
rgba
(
36
,
16
,
95
,
.18
),
0
4px
12px
rgba
(
36
,
16
,
95
,
.07
);
backdrop-filter
:
blur
(
18px
);
animation
:
site-menu-in
.16s
ease-out
}
@keyframes
site-menu-in
{
from
{
opacity
:
0
;
transform
:
translateY
(
-5px
)
scale
(
.985
)}
to
{
opacity
:
1
;
transform
:
none
}}
.site-menu-heading
{
height
:
28px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
padding
:
0
7px
;
color
:
var
(
--muted
);
font-size
:
8px
;
font-weight
:
750
;
letter-spacing
:
.04em
}
.site-menu-heading
em
{
display
:
grid
;
place-items
:
center
;
width
:
19px
;
height
:
19px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
6px
;
font-size
:
8px
;
font-style
:
normal
}
.site-menu-list
{
display
:
flex
;
flex-direction
:
column
;
gap
:
3px
}
.site-menu-list
>
button
{
width
:
100%
;
min-width
:
0
;
height
:
52px
;
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
padding
:
6px
8px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
transparent
;
border
:
0
;
border-radius
:
10px
}
.site-menu-list
>
button
:hover
{
background
:
#f7f5ff
}
.site-menu-list
>
button
.selected
{
background
:
#f4f0ff
}
.site-menu-avatar
{
width
:
32px
;
height
:
32px
;
border-radius
:
9px
;
font-size
:
11px
}
.site-menu-copy
{
flex
:
1
;
gap
:
4px
}
.site-menu-copy
strong
{
overflow
:
hidden
;
font-size
:
10px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-menu-copy
small
{
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.site-menu-status
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#d0d1d8
;
box-shadow
:
0
0
0
3px
rgba
(
208
,
209
,
216
,
.15
)}
.site-menu-status.ready
{
background
:
#7957d5
;
box-shadow
:
0
0
0
3px
rgba
(
121
,
87
,
213
,
.1
)}
.site-menu-status.published
{
background
:
#20b486
;
box-shadow
:
0
0
0
3px
rgba
(
32
,
180
,
134
,
.1
)}
.site-menu-status.failed
{
background
:
#d92d20
;
box-shadow
:
0
0
0
3px
rgba
(
217
,
45
,
32
,
.1
)}
.site-menu-list
>
button
>
svg
{
flex
:
0
0
auto
;
color
:
var
(
--primary
)}
.site-switcher-menu
>
p
{
height
:
27px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin
:
6px
3px
0
;
padding
:
6px
5px
0
;
color
:
var
(
--muted
);
border-top
:
1px
solid
var
(
--divider
);
font-size
:
7px
}
.panel-tabs
{
flex
:
0
0
49px
;
height
:
49px
;
padding
:
0
14px
;
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
5px
;
align-items
:
center
;
border-bottom
:
1px
solid
var
(
--divider
)}
.panel-tabs
button
{
height
:
35px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
7px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
9px
;
font-size
:
11px
;
font-weight
:
700
}
.panel-tabs
button
.active
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.chat-scroll
{
flex
:
1
1
0
;
min-height
:
0
;
overflow-x
:
hidden
;
overflow-y
:
auto
;
overscroll-behavior
:
contain
;
padding
:
20px
17px
28px
}
.chat-date
{
text-align
:
center
;
color
:
#aaaeba
;
font-size
:
9px
;
margin-bottom
:
22px
}
.message
{
display
:
flex
;
gap
:
9px
;
margin-bottom
:
17px
}
.message.user
{
justify-content
:
flex-end
}
.message
>
div
{
max-width
:
86%
}
.message
p
{
margin
:
0
;
padding
:
12px
14px
;
color
:
var
(
--text-2
);
background
:
#f6f6f9
;
border-radius
:
4px
13px
13px
13px
;
font-size
:
12px
;
line-height
:
1.65
}
.message.user
p
{
color
:
white
;
background
:
var
(
--gradient
);
border-radius
:
13px
4px
13px
13px
;
box-shadow
:
0
8px
18px
rgba
(
112
,
40
,
255
,
.15
)}
.message
small
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
margin
:
6px
2px
0
;
color
:
#9a9da8
;
font-size
:
9px
}
.agent-avatar
{
flex
:
0
0
auto
;
display
:
grid
;
place-items
:
center
;
width
:
28px
;
height
:
28px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border
:
1px
solid
#e2dbff
;
border-radius
:
9px
}
.agent-progress
{
display
:
flex
;
gap
:
9px
;
margin
:
8px
0
18px
}
.agent-progress
>
div
{
flex
:
1
;
padding
:
12px
13px
;
background
:
#faf9ff
;
border
:
1px
solid
#ebe6ff
;
border-radius
:
4px
13px
13px
13px
}
.agent-progress
p
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin
:
0
0
10px
;
color
:
var
(
--text-2
);
font-size
:
11px
;
font-weight
:
700
}
.agent-progress
small
{
color
:
var
(
--muted
);
font-size
:
9px
}
.progress-track
{
height
:
3px
;
margin-bottom
:
8px
;
overflow
:
hidden
;
background
:
#ebe7f5
;
border-radius
:
9px
}
.progress-track
i
{
display
:
block
;
width
:
45%
;
height
:
100%
;
background
:
var
(
--gradient
);
border-radius
:
9px
;
animation
:
progress
1.5s
ease-in-out
infinite
}
@keyframes
progress
{
50
%
{
transform
:
translateX
(
120%
)}}
.suggestions
{
display
:
flex
;
flex-direction
:
column
;
gap
:
7px
;
margin
:
25px
37px
0
}
.suggestions
>
span
{
margin-bottom
:
3px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.suggestions
button
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
9px
11px
;
text-align
:
left
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
font-size
:
10px
}
.suggestions
button
:hover
{
color
:
var
(
--primary
);
border-color
:
#cdbdff
;
background
:
#faf9ff
}
.chat-scroll
{
flex
:
1
1
0
;
min-height
:
0
;
overflow-x
:
hidden
;
overflow-y
:
auto
;
overscroll-behavior
:
contain
;
padding
:
20px
17px
28px
}
.chat-date
{
text-align
:
center
;
color
:
#aaaeba
;
font-size
:
9px
;
margin-bottom
:
22px
}
.message
{
display
:
flex
;
gap
:
9px
;
margin-bottom
:
17px
}
.message.user
{
justify-content
:
flex-end
}
.message
>
div
{
max-width
:
86%
}
.message
p
{
margin
:
0
;
padding
:
12px
14px
;
color
:
var
(
--text-2
);
background
:
#f6f6f9
;
border-radius
:
4px
13px
13px
13px
;
font-size
:
12px
;
line-height
:
1.65
}
.message.user
p
{
color
:
white
;
background
:
var
(
--gradient
);
border-radius
:
13px
4px
13px
13px
;
box-shadow
:
0
8px
18px
rgba
(
112
,
40
,
255
,
.15
)}
.message
small
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
margin
:
6px
2px
0
;
color
:
#9a9da8
;
font-size
:
9px
}
.agent-avatar
{
flex
:
0
0
auto
;
display
:
grid
;
place-items
:
center
;
width
:
28px
;
height
:
28px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border
:
1px
solid
#e2dbff
;
border-radius
:
9px
}
.agent-progress
{
display
:
flex
;
gap
:
9px
;
margin
:
8px
0
18px
}
.agent-progress
>
div
{
flex
:
1
;
padding
:
12px
13px
;
background
:
#faf9ff
;
border
:
1px
solid
#ebe6ff
;
border-radius
:
4px
13px
13px
13px
}
.agent-progress
p
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin
:
0
0
10px
;
color
:
var
(
--text-2
);
font-size
:
11px
;
font-weight
:
700
}
.agent-progress
small
{
color
:
var
(
--muted
);
font-size
:
9px
}
.progress-track
{
height
:
3px
;
margin-bottom
:
8px
;
overflow
:
hidden
;
background
:
#ebe7f5
;
border-radius
:
9px
}
.progress-track
i
{
display
:
block
;
width
:
45%
;
height
:
100%
;
background
:
var
(
--gradient
);
border-radius
:
9px
;
animation
:
progress
1.5s
ease-in-out
infinite
}
@keyframes
progress
{
50
%
{
transform
:
translateX
(
120%
)}}
.suggestions
{
display
:
flex
;
flex-direction
:
column
;
gap
:
7px
;
margin
:
25px
37px
0
}
.suggestions
>
span
{
margin-bottom
:
3px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.suggestions
button
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
9px
11px
;
text-align
:
left
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
font-size
:
10px
}
.suggestions
button
:hover
{
color
:
var
(
--primary
);
border-color
:
#cdbdff
;
background
:
#faf9ff
}
.composer
{
flex
:
0
0
auto
;
margin
:
0
14px
14px
;
padding
:
10px
;
background
:
#fff
;
border
:
1px
solid
#dcd8e8
;
border-radius
:
13px
;
box-shadow
:
0
5px
22px
rgba
(
36
,
16
,
95
,
.07
)}
.composer
textarea
{
width
:
100%
;
padding
:
2px
4px
;
resize
:
none
;
color
:
var
(
--text
);
background
:
transparent
;
border
:
0
;
outline
:
none
;
font-size
:
11px
;
line-height
:
1.55
}
.composer
textarea
::placeholder
{
color
:
#a5a7b0
}
.composer
>
div
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
}
.composer
>
div
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.online-dot
{
width
:
6px
;
height
:
6px
;
border-radius
:
50%
;
background
:
#20b486
}
.composer
button
{
display
:
grid
;
place-items
:
center
;
width
:
29px
;
height
:
29px
;
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
;
border-radius
:
8px
}
.composer
{
flex
:
0
0
auto
;
margin
:
0
14px
14px
;
padding
:
10px
;
background
:
#fff
;
border
:
1px
solid
#dcd8e8
;
border-radius
:
13px
;
box-shadow
:
0
5px
22px
rgba
(
36
,
16
,
95
,
.07
)}
.composer
textarea
{
width
:
100%
;
padding
:
2px
4px
;
resize
:
none
;
color
:
var
(
--text
);
background
:
transparent
;
border
:
0
;
outline
:
none
;
font-size
:
11px
;
line-height
:
1.55
}
.composer
textarea
::placeholder
{
color
:
#a5a7b0
}
.composer
>
div
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
}
.composer
>
div
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.online-dot
,
.draft-dot
{
width
:
6px
;
height
:
6px
;
border-radius
:
50%
}
.online-dot
{
background
:
#20b486
}
.draft-dot
{
background
:
#d79614
;
box-shadow
:
0
0
0
3px
rgba
(
215
,
150
,
20
,
.1
)
}
.composer
button
{
display
:
grid
;
place-items
:
center
;
width
:
29px
;
height
:
29px
;
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
;
border-radius
:
8px
}
.history-list
{
flex
:
1
1
0
;
min-height
:
0
;
overflow
:
auto
;
padding
:
16px
16px
22px
}
.environment-versions
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
8px
;
margin-bottom
:
22px
}
.environment-versions
>
button
{
min-width
:
0
;
padding
:
12px
;
text-align
:
left
;
background
:
#fafafd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
12px
}
.environment-versions
>
button
:hover
,
.environment-versions
>
button
.active
{
border-color
:
#bca8ff
;
background
:
#faf8ff
;
box-shadow
:
0
5px
18px
rgba
(
112
,
40
,
255
,
.08
)}
.environment-versions
>
button
.production
:hover
,
.environment-versions
>
button
.production.active
{
border-color
:
#9bd8c6
;
background
:
#f4fcf9
;
box-shadow
:
0
5px
18px
rgba
(
32
,
180
,
134
,
.08
)}
.environment-versions
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
750
}
.environment-versions
span
>
em
{
margin-left
:
auto
;
padding
:
2px
4px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
4px
;
font-size
:
7px
;
font-style
:
normal
}
.environment-versions
.production
span
>
em
{
color
:
#13765b
;
background
:
#e7f8f2
}
.environment-versions
strong
,
.environment-versions
small
{
display
:
block
}
.environment-versions
strong
{
margin-top
:
13px
;
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
font-size
:
13px
;
letter-spacing
:
.02em
}
.environment-versions
small
{
margin-top
:
5px
;
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.history-heading
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
18px
}
.history-heading
h3
{
margin
:
0
0
4px
;
font-size
:
14px
}
.history-heading
p
{
margin
:
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.history-heading
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
8px
;
font-size
:
10px
;
font-weight
:
800
}
.history-list
article
{
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
7px
;
min-height
:
69px
}
.history-list
article
.environment-version
{
margin
:
0
-5px
;
padding
:
0
5px
;
background
:
#fbfaff
;
border-radius
:
10px
}
.timeline
{
display
:
flex
;
align-items
:
center
;
flex-direction
:
column
}
.timeline
i
{
display
:
grid
;
place-items
:
center
;
width
:
17px
;
height
:
17px
;
color
:
white
;
background
:
#d6d7df
;
border
:
3px
solid
white
;
box-shadow
:
0
0
0
1px
#d6d7df
;
border-radius
:
50%
;
font-style
:
normal
}
.history-list
article
.environment-version
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.timeline
span
{
width
:
1px
;
flex
:
1
;
background
:
var
(
--divider
)}
.commit-info
{
min-width
:
0
;
padding-bottom
:
18px
}
.commit-info
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.preview-badge
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.commit-info
.production-badge
{
color
:
#13765b
;
background
:
#e7f8f2
}
.commit-info
p
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin
:
5px
0
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.commit-info
code
{
font-size
:
9px
}
.history-actions
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
4px
}
.history-actions
button
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
padding
:
0
;
color
:
var
(
--muted
);
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
7px
}
.history-actions
button
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.history-list
{
flex
:
1
1
0
;
min-height
:
0
;
overflow
:
auto
;
padding
:
16px
16px
22px
}
.environment-versions
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
8px
;
margin-bottom
:
22px
}
.environment-versions
>
button
{
min-width
:
0
;
padding
:
12px
;
text-align
:
left
;
background
:
#fafafd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
12px
}
.environment-versions
>
button
:hover
,
.environment-versions
>
button
.active
{
border-color
:
#bca8ff
;
background
:
#faf8ff
;
box-shadow
:
0
5px
18px
rgba
(
112
,
40
,
255
,
.08
)}
.environment-versions
>
button
.production
:hover
,
.environment-versions
>
button
.production.active
{
border-color
:
#9bd8c6
;
background
:
#f4fcf9
;
box-shadow
:
0
5px
18px
rgba
(
32
,
180
,
134
,
.08
)}
.environment-versions
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
750
}
.environment-versions
span
>
em
{
margin-left
:
auto
;
padding
:
2px
4px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
4px
;
font-size
:
7px
;
font-style
:
normal
}
.environment-versions
.production
span
>
em
{
color
:
#13765b
;
background
:
#e7f8f2
}
.environment-versions
strong
,
.environment-versions
small
{
display
:
block
}
.environment-versions
strong
{
margin-top
:
13px
;
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
font-size
:
13px
;
letter-spacing
:
.02em
}
.environment-versions
small
{
margin-top
:
5px
;
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.history-heading
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
18px
}
.history-heading
h3
{
margin
:
0
0
4px
;
font-size
:
14px
}
.history-heading
p
{
margin
:
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.history-heading
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
8px
;
font-size
:
10px
;
font-weight
:
800
}
.history-list
article
{
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
7px
;
min-height
:
69px
}
.history-list
article
.environment-version
{
margin
:
0
-5px
;
padding
:
0
5px
;
background
:
#fbfaff
;
border-radius
:
10px
}
.timeline
{
display
:
flex
;
align-items
:
center
;
flex-direction
:
column
}
.timeline
i
{
display
:
grid
;
place-items
:
center
;
width
:
17px
;
height
:
17px
;
color
:
white
;
background
:
#d6d7df
;
border
:
3px
solid
white
;
box-shadow
:
0
0
0
1px
#d6d7df
;
border-radius
:
50%
;
font-style
:
normal
}
.history-list
article
.environment-version
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.timeline
span
{
width
:
1px
;
flex
:
1
;
background
:
var
(
--divider
)}
.commit-info
{
min-width
:
0
;
padding-bottom
:
18px
}
.commit-info
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.preview-badge
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.commit-info
.production-badge
{
color
:
#13765b
;
background
:
#e7f8f2
}
.commit-info
p
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin
:
5px
0
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.commit-info
code
{
font-size
:
9px
}
.history-actions
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
4px
}
.history-actions
button
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
padding
:
0
;
color
:
var
(
--muted
);
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
7px
}
.history-actions
button
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.preview-shell
{
min-height
:
0
;
min-width
:
0
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
}
.preview-toolbar
{
min-height
:
66px
;
display
:
grid
;
grid-template-columns
:
minmax
(
190px
,
1
fr
)
auto
minmax
(
310px
,
1
fr
);
grid-template-areas
:
"title modes actions"
;
align-items
:
center
;
gap
:
12px
;
padding
:
0
18px
;
background
:
#fff
;
border-bottom
:
1px
solid
var
(
--border
)}
.preview-title
{
grid-area
:
title
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
min-width
:
0
}
.preview-title
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
}
.preview-title
strong
{
overflow
:
hidden
;
font-size
:
12px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.preview-title
em
{
flex
:
0
0
auto
;
padding
:
3px
6px
;
color
:
#7957d5
;
background
:
#f2edff
;
border-radius
:
5px
;
font-size
:
8px
;
font-style
:
normal
;
font-weight
:
750
}
.preview-title
em
.production
{
color
:
#13765b
;
background
:
#e7f8f2
}
.status-dot
{
flex
:
0
0
auto
;
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#7957d5
;
box-shadow
:
0
0
0
4px
rgba
(
112
,
40
,
255
,
.1
)}
.status-dot.production
{
background
:
#20b486
;
box-shadow
:
0
0
0
4px
rgba
(
32
,
180
,
134
,
.1
)}
.build-status
{
flex
:
0
0
auto
;
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
padding
:
5px
7px
;
color
:
#188866
;
background
:
#ebfbf5
;
border-radius
:
7px
;
font-size
:
8px
;
font-weight
:
750
}
.build-status.failed
{
color
:
#b42318
;
background
:
#fff0ee
}
.build-status.unpublished
{
color
:
#986600
;
background
:
#fff8e5
}
.preview-modes
{
grid-area
:
modes
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
}
.environment-toggle
,
.device-toggle
{
display
:
flex
;
padding
:
3px
;
background
:
#f2f2f6
;
border-radius
:
8px
}
.environment-toggle
button
{
height
:
27px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
;
font-size
:
8px
;
font-weight
:
750
}
.environment-toggle
button
.active
,
.device-toggle
button
.active
{
color
:
var
(
--text
);
background
:
#fff
;
box-shadow
:
0
2px
7px
rgba
(
36
,
16
,
95
,
.1
)}
.device-toggle
button
{
display
:
grid
;
place-items
:
center
;
width
:
31px
;
height
:
27px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
}
.toolbar-actions
{
grid-area
:
actions
;
display
:
flex
;
justify-content
:
flex-end
;
gap
:
6px
;
min-width
:
0
}
.toolbar-actions
button
,
.toolbar-actions
a
{
height
:
31px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
font-size
:
8px
;
font-weight
:
750
;
white-space
:
nowrap
}
.toolbar-actions
button
:hover:not
(
:disabled
),
.toolbar-actions
a
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.toolbar-actions
.environment-link.production-link
{
color
:
#13765b
}
.toolbar-actions
.environment-link
:disabled
{
color
:
#a8a9b0
;
background
:
#f5f5f7
}
.toolbar-actions
.rebuild-button
{
color
:
var
(
--primary
);
border-color
:
#d8ccff
;
background
:
#faf8ff
}
.toolbar-actions
.undo-button
{
color
:
#9a6100
;
border-color
:
#ecd8ad
;
background
:
#fffaf0
}
.toolbar-actions
.publish-button
{
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
}
.toolbar-actions
.publish-button
:hover:not
(
:disabled
)
{
color
:
white
;
background
:
var
(
--gradient
);
box-shadow
:
0
7px
17px
rgba
(
112
,
40
,
255
,
.2
)}
.toolbar-actions
.publish-button
:disabled
{
color
:
#9a9da8
;
background
:
#efeff4
;
border
:
1px
solid
#e1e1e7
;
box-shadow
:
none
}
.preview-shell
{
min-height
:
0
;
min-width
:
0
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
}
.preview-toolbar
{
min-height
:
66px
;
display
:
grid
;
grid-template-columns
:
minmax
(
190px
,
1
fr
)
auto
minmax
(
310px
,
1
fr
);
grid-template-areas
:
"title modes actions"
;
align-items
:
center
;
gap
:
12px
;
padding
:
0
18px
;
background
:
#fff
;
border-bottom
:
1px
solid
var
(
--border
)}
.preview-title
{
grid-area
:
title
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
min-width
:
0
}
.preview-title
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
}
.preview-title
strong
{
overflow
:
hidden
;
font-size
:
12px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.preview-title
em
{
flex
:
0
0
auto
;
padding
:
3px
6px
;
color
:
#7957d5
;
background
:
#f2edff
;
border-radius
:
5px
;
font-size
:
8px
;
font-style
:
normal
;
font-weight
:
750
}
.preview-title
em
.production
{
color
:
#13765b
;
background
:
#e7f8f2
}
.status-dot
{
flex
:
0
0
auto
;
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#7957d5
;
box-shadow
:
0
0
0
4px
rgba
(
112
,
40
,
255
,
.1
)}
.status-dot.production
{
background
:
#20b486
;
box-shadow
:
0
0
0
4px
rgba
(
32
,
180
,
134
,
.1
)}
.build-status
{
flex
:
0
0
auto
;
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
padding
:
5px
7px
;
color
:
#188866
;
background
:
#ebfbf5
;
border-radius
:
7px
;
font-size
:
8px
;
font-weight
:
750
}
.build-status.failed
{
color
:
#b42318
;
background
:
#fff0ee
}
.build-status.unpublished
{
color
:
#986600
;
background
:
#fff8e5
}
.preview-modes
{
grid-area
:
modes
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
}
.environment-toggle
,
.device-toggle
{
display
:
flex
;
padding
:
3px
;
background
:
#f2f2f6
;
border-radius
:
8px
}
.environment-toggle
button
{
height
:
27px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
;
font-size
:
8px
;
font-weight
:
750
}
.environment-toggle
button
.active
,
.device-toggle
button
.active
{
color
:
var
(
--text
);
background
:
#fff
;
box-shadow
:
0
2px
7px
rgba
(
36
,
16
,
95
,
.1
)}
.device-toggle
button
{
display
:
grid
;
place-items
:
center
;
width
:
31px
;
height
:
27px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
}
.toolbar-actions
{
grid-area
:
actions
;
display
:
flex
;
justify-content
:
flex-end
;
gap
:
6px
;
min-width
:
0
}
.toolbar-actions
button
,
.toolbar-actions
a
{
height
:
31px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
font-size
:
8px
;
font-weight
:
750
;
white-space
:
nowrap
}
.toolbar-actions
button
:hover:not
(
:disabled
),
.toolbar-actions
a
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.toolbar-actions
.environment-link.production-link
{
color
:
#13765b
}
.toolbar-actions
.environment-link
:disabled
{
color
:
#a8a9b0
;
background
:
#f5f5f7
}
.toolbar-actions
.rebuild-button
{
color
:
var
(
--primary
);
border-color
:
#d8ccff
;
background
:
#faf8ff
}
.toolbar-actions
.undo-button
{
color
:
#9a6100
;
border-color
:
#ecd8ad
;
background
:
#fffaf0
}
.toolbar-actions
.publish-button
{
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
}
.toolbar-actions
.publish-button
:hover:not
(
:disabled
)
{
color
:
white
;
background
:
var
(
--gradient
);
box-shadow
:
0
7px
17px
rgba
(
112
,
40
,
255
,
.2
)}
.toolbar-actions
.publish-button
:disabled
{
color
:
#9a9da8
;
background
:
#efeff4
;
border
:
1px
solid
#e1e1e7
;
box-shadow
:
none
}
.history-edit-lock
{
flex
:
0
0
auto
;
margin
:
0
14px
10px
;
padding
:
9px
11px
;
color
:
#6f4b00
;
background
:
#fff8e7
;
border
:
1px
solid
#f0dfb8
;
border-radius
:
10px
;
font-size
:
9px
;
line-height
:
1.5
}
.history-edit-lock
>
div
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
7px
}
.history-edit-lock
svg
{
flex
:
0
0
auto
;
margin-top
:
1px
}
.history-panel-lock
{
margin
:
0
0
14px
}
.history-list
article
{
display
:
block
;
min-height
:
0
;
margin-bottom
:
5px
}
.history-list
article
.selected
{
background
:
#faf8ff
;
border-radius
:
11px
}
.history-row
{
width
:
100%
;
min-width
:
0
;
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
8px
;
padding
:
9px
8px
5px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
transparent
;
border
:
1px
solid
transparent
;
border-radius
:
11px
}
.history-list
article
.selected
.history-row
{
border-color
:
#ded4ff
}
.history-row
.timeline
{
min-height
:
49px
}
.history-list
article
.selected
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.history-row
.commit-info
{
display
:
block
;
min-width
:
0
;
padding
:
0
0
10px
}
.commit-info
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
>
span
>
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.current-badge
{
color
:
#775400
;
background
:
#fff2c7
}
.commit-info
>
small
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin-top
:
6px
;
color
:
var
(
--muted
);
font-size
:
8px
}
.commit-info
>
small
code
{
font-size
:
8px
}
.history-row-state
,
.history-preview-button
{
align-self
:
start
;
min-height
:
24px
;
padding
:
4px
7px
;
border-radius
:
6px
;
font-size
:
7px
;
font-weight
:
750
}
.history-row-state
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.history-preview-button
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
)}
.history-preview-button
:hover:not
(
:disabled
)
{
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-color
:
#d8ccff
}
.history-edit-lock
{
flex
:
0
0
auto
;
margin
:
0
14px
10px
;
padding
:
9px
11px
;
color
:
#6f4b00
;
background
:
#fff8e7
;
border
:
1px
solid
#f0dfb8
;
border-radius
:
10px
;
font-size
:
9px
;
line-height
:
1.5
}
.history-edit-lock
>
div
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
7px
}
.history-edit-lock
svg
{
flex
:
0
0
auto
;
margin-top
:
1px
}
.history-panel-lock
{
margin
:
0
0
14px
}
.history-list
article
{
display
:
block
;
min-height
:
0
;
margin-bottom
:
5px
}
.history-list
article
.selected
{
background
:
#faf8ff
;
border-radius
:
11px
}
.history-row
{
width
:
100%
;
min-width
:
0
;
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
8px
;
padding
:
9px
8px
5px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
transparent
;
border
:
1px
solid
transparent
;
border-radius
:
11px
}
.history-list
article
.selected
.history-row
{
border-color
:
#ded4ff
}
.history-row
.timeline
{
min-height
:
49px
}
.history-list
article
.selected
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.history-row
.commit-info
{
display
:
block
;
min-width
:
0
;
padding
:
0
0
10px
}
.commit-info
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
>
span
>
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.current-badge
{
color
:
#775400
;
background
:
#fff2c7
}
.commit-info
>
small
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin-top
:
6px
;
color
:
var
(
--muted
);
font-size
:
8px
}
.commit-info
>
small
code
{
font-size
:
8px
}
.history-row-state
,
.history-preview-button
{
align-self
:
start
;
min-height
:
24px
;
padding
:
4px
7px
;
border-radius
:
6px
;
font-size
:
7px
;
font-weight
:
750
}
.history-row-state
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.history-preview-button
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
)}
.history-preview-button
:hover:not
(
:disabled
)
{
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-color
:
#d8ccff
}
.version-state-strip
{
flex
:
0
0
70px
;
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
background
:
#fff
;
border-bottom
:
1px
solid
var
(
--border
)}
.version-state-strip
article
{
min-width
:
0
;
padding
:
10px
16px
;
border-right
:
1px
solid
var
(
--divider
)}
.version-state-strip
article
:last-child
{
border-right
:
0
}
.version-state-strip
article
>
span
,
.version-state-strip
strong
,
.version-state-strip
small
{
display
:
block
;
overflow
:
hidden
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.version-state-strip
article
>
span
{
color
:
var
(
--muted
);
font-size
:
8px
;
font-weight
:
750
}
.version-state-strip
article
>
span
em
{
margin-left
:
4px
;
padding
:
2px
5px
;
color
:
#6a4bb8
;
background
:
#f2edff
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.version-state-strip
strong
{
margin-top
:
5px
;
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
font-size
:
11px
}
.version-state-strip
small
{
margin-top
:
3px
;
color
:
var
(
--muted
);
font-size
:
7px
}
.version-state-strip
article
.error
{
background
:
#fffafa
}
.version-state-strip
article
.error
>
span
em
{
color
:
#b42318
;
background
:
#fff0ee
}
.version-state-strip
article
.production
{
background
:
#fbfffd
}
.version-state-strip
article
.production
strong
{
color
:
#13765b
}
.history-edit-lock
{
flex
:
0
0
auto
;
margin
:
0
14px
10px
;
padding
:
9px
11px
;
color
:
#6f4b00
;
background
:
#fff8e7
;
border
:
1px
solid
#f0dfb8
;
border-radius
:
10px
;
font-size
:
9px
;
line-height
:
1.5
}
.history-edit-lock
>
div
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
7px
}
.history-edit-lock
svg
{
flex
:
0
0
auto
;
margin-top
:
1px
}
.history-panel-lock
{
margin
:
0
0
14px
}
.draft-card
{
margin
:
0
0
14px
;
padding
:
12px
;
background
:
#fffaf0
;
border
:
1px
solid
#ecd8ad
;
border-radius
:
12px
}
.draft-card-heading
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
}
.draft-card-heading
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
28px
;
height
:
28px
;
flex
:
0
0
auto
;
color
:
#9a6100
;
background
:
#fff1cf
;
border-radius
:
8px
}
.draft-card-heading
>
div
{
min-width
:
0
}
.draft-card-heading
strong
,
.draft-card-heading
small
{
display
:
block
}
.draft-card-heading
strong
{
font-size
:
10px
}
.draft-card-heading
small
{
margin-top
:
4px
;
overflow
:
hidden
;
color
:
#8a6a30
;
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.draft-card-actions
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
6px
;
margin-top
:
11px
}
.draft-card-actions
button
{
height
:
29px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
color
:
#765421
;
background
:
#fff
;
border
:
1px
solid
#ecd8ad
;
border-radius
:
7px
;
font-size
:
8px
;
font-weight
:
750
}
.draft-card-actions
button
:last-child
{
color
:
white
;
background
:
#b87808
;
border-color
:
#b87808
}
.draft-card-actions
button
:hover:not
(
:disabled
)
{
box-shadow
:
0
4px
12px
rgba
(
154
,
97
,
0
,
.12
)}
.history-list
article
{
display
:
block
;
min-height
:
0
;
margin-bottom
:
5px
}
.history-list
article
.selected
{
background
:
#faf8ff
;
border-radius
:
11px
}
.history-row
{
width
:
100%
;
min-width
:
0
;
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
8px
;
padding
:
9px
8px
5px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
transparent
;
border
:
1px
solid
transparent
;
border-radius
:
11px
}
.history-list
article
.selected
.history-row
{
border-color
:
#ded4ff
}
.history-row
.timeline
{
min-height
:
49px
}
.history-list
article
.selected
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.history-row
.commit-info
{
display
:
block
;
min-width
:
0
;
padding
:
0
0
10px
}
.commit-info
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
>
span
>
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.current-badge
{
color
:
#775400
;
background
:
#fff2c7
}
.commit-info
>
small
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin-top
:
6px
;
color
:
var
(
--muted
);
font-size
:
8px
}
.commit-info
>
small
code
{
font-size
:
8px
}
.history-row-state
,
.history-preview-button
{
align-self
:
start
;
min-height
:
24px
;
padding
:
4px
7px
;
border-radius
:
6px
;
font-size
:
7px
;
font-weight
:
750
}
.history-row-state
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.history-preview-button
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
)}
.history-preview-button
:hover:not
(
:disabled
)
{
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-color
:
#d8ccff
}
.browser-stage
{
position
:
relative
;
flex
:
1
;
min-height
:
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
25px
;
background
:
#eff0f5
;
overflow
:
auto
}
.browser-frame
{
position
:
relative
;
width
:
100%
;
height
:
100%
;
overflow
:
hidden
;
background
:
#fff
;
border
:
1px
solid
#dfe0e7
;
border-radius
:
11px
;
box-shadow
:
0
15px
45px
rgba
(
30
,
24
,
53
,
.1
);
transition
:
.35s
ease
}
.browser-frame.mobile
{
width
:
390px
;
max-height
:
760px
}
.browser-chrome
{
height
:
31px
;
display
:
grid
;
grid-template-columns
:
70px
1
fr
70px
;
align-items
:
center
;
padding
:
0
10px
;
background
:
#fafafd
;
border-bottom
:
1px
solid
#ebeaf0
}
.browser-chrome
>
div
{
display
:
flex
;
gap
:
5px
}
.browser-chrome
i
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#e1e1e7
}
.browser-chrome
i
:first-child
{
background
:
#ff8b86
}
.browser-chrome
i
:nth-child
(
2
)
{
background
:
#ffd36b
}
.browser-chrome
i
:nth-child
(
3
)
{
background
:
#71d19a
}
.browser-chrome
span
{
justify-self
:
center
;
width
:
min
(
390px
,
80%
);
padding
:
4px
10px
;
text-align
:
center
;
color
:
#9a9ca6
;
background
:
#f0f0f4
;
border-radius
:
5px
;
font-size
:
8px
}
.browser-chrome
>
svg
{
justify-self
:
end
;
color
:
#a7a9b3
}
.browser-frame
iframe
{
display
:
block
;
width
:
100%
;
height
:
calc
(
100%
-
31px
);
border
:
0
;
background
:
#fff
}
.production-empty
{
height
:
calc
(
100%
-
31px
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
padding
:
30px
;
text-align
:
center
;
background
:
linear-gradient
(
180deg
,
#fbfffd
,
#f7faf9
)}
.production-empty
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
52px
;
height
:
52px
;
color
:
#168064
;
background
:
#e8f8f2
;
border-radius
:
16px
}
.production-empty
strong
{
margin-top
:
17px
;
font-size
:
15px
}
.production-empty
p
{
max-width
:
360px
;
margin
:
8px
0
20px
;
color
:
var
(
--muted
);
font-size
:
10px
;
line-height
:
1.7
}
.production-empty
button
{
height
:
36px
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
0
14px
;
color
:
white
;
background
:
linear-gradient
(
135deg
,
#15936e
,
#20b486
);
border
:
0
;
border-radius
:
9px
;
font-size
:
10px
;
font-weight
:
800
}
.preview-busy
{
position
:
absolute
;
inset
:
31px
0
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
gap
:
8px
;
color
:
var
(
--primary
);
background
:
rgba
(
248
,
248
,
252
,
.72
);
backdrop-filter
:
blur
(
4px
)}
.preview-busy
strong
{
color
:
var
(
--text
);
font-size
:
13px
}
.preview-busy
span
{
color
:
var
(
--muted
);
font-size
:
10px
}
.preview-footer
{
height
:
29px
;
padding
:
0
22px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
color
:
var
(
--muted
);
background
:
#fff
;
border-top
:
1px
solid
var
(
--divider
);
font-size
:
8px
}
.preview-footer
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
}
.browser-stage
{
position
:
relative
;
flex
:
1
;
min-height
:
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
25px
;
background
:
#eff0f5
;
overflow
:
auto
}
.browser-frame
{
position
:
relative
;
width
:
100%
;
height
:
100%
;
overflow
:
hidden
;
background
:
#fff
;
border
:
1px
solid
#dfe0e7
;
border-radius
:
11px
;
box-shadow
:
0
15px
45px
rgba
(
30
,
24
,
53
,
.1
);
transition
:
.35s
ease
}
.browser-frame.mobile
{
width
:
390px
;
max-height
:
760px
}
.browser-chrome
{
height
:
31px
;
display
:
grid
;
grid-template-columns
:
70px
1
fr
70px
;
align-items
:
center
;
padding
:
0
10px
;
background
:
#fafafd
;
border-bottom
:
1px
solid
#ebeaf0
}
.browser-chrome
>
div
{
display
:
flex
;
gap
:
5px
}
.browser-chrome
i
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#e1e1e7
}
.browser-chrome
i
:first-child
{
background
:
#ff8b86
}
.browser-chrome
i
:nth-child
(
2
)
{
background
:
#ffd36b
}
.browser-chrome
i
:nth-child
(
3
)
{
background
:
#71d19a
}
.browser-chrome
span
{
justify-self
:
center
;
width
:
min
(
390px
,
80%
);
padding
:
4px
10px
;
text-align
:
center
;
color
:
#9a9ca6
;
background
:
#f0f0f4
;
border-radius
:
5px
;
font-size
:
8px
}
.browser-chrome
>
svg
{
justify-self
:
end
;
color
:
#a7a9b3
}
.browser-frame
iframe
{
display
:
block
;
width
:
100%
;
height
:
calc
(
100%
-
31px
);
border
:
0
;
background
:
#fff
}
.production-empty
{
height
:
calc
(
100%
-
31px
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
padding
:
30px
;
text-align
:
center
;
background
:
linear-gradient
(
180deg
,
#fbfffd
,
#f7faf9
)}
.production-empty
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
52px
;
height
:
52px
;
color
:
#168064
;
background
:
#e8f8f2
;
border-radius
:
16px
}
.production-empty
strong
{
margin-top
:
17px
;
font-size
:
15px
}
.production-empty
p
{
max-width
:
360px
;
margin
:
8px
0
20px
;
color
:
var
(
--muted
);
font-size
:
10px
;
line-height
:
1.7
}
.production-empty
button
{
height
:
36px
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
0
14px
;
color
:
white
;
background
:
linear-gradient
(
135deg
,
#15936e
,
#20b486
);
border
:
0
;
border-radius
:
9px
;
font-size
:
10px
;
font-weight
:
800
}
.preview-busy
{
position
:
absolute
;
inset
:
31px
0
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
gap
:
8px
;
color
:
var
(
--primary
);
background
:
rgba
(
248
,
248
,
252
,
.72
);
backdrop-filter
:
blur
(
4px
)}
.preview-busy
strong
{
color
:
var
(
--text
);
font-size
:
13px
}
.preview-busy
span
{
color
:
var
(
--muted
);
font-size
:
10px
}
.preview-footer
{
height
:
29px
;
padding
:
0
22px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
18px
;
color
:
var
(
--muted
);
background
:
#fff
;
border-top
:
1px
solid
var
(
--divider
);
font-size
:
8px
}
.preview-footer
span
{
min-width
:
0
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
overflow
:
hidden
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.preview-footer
span
:last-child
{
flex
:
0
1
auto
}
@media
(
max-width
:
1100px
){
.create-layout
{
grid-template-columns
:
1
fr
;
gap
:
48px
;
padding-top
:
40px
}
.create-intro
{
text-align
:
center
}
.create-intro
>
p
,
.create-intro
h1
{
margin-left
:
auto
;
margin-right
:
auto
}
.intro-tag
{
margin
:
auto
}
.feature-row
{
max-width
:
620px
;
margin-left
:
auto
;
margin-right
:
auto
}
.create-card-wrap
{
justify-content
:
center
}
.workspace
{
grid-template-columns
:
58px
340px
1
fr
}}
.history-row-state
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
}
.rail-bottom
,
.rail-logout
{
display
:
none
}
.rail
button
{
position
:
relative
}
.rail
button
[
data-tooltip
]
::after
{
content
:
attr
(
data-tooltip
);
position
:
absolute
;
z-index
:
50
;
left
:
48px
;
top
:
50%
;
width
:
max-content
;
max-width
:
150px
;
padding
:
6px
9px
;
color
:
white
;
background
:
#282435
;
border-radius
:
7px
;
box-shadow
:
0
7px
18px
rgba
(
25
,
20
,
42
,
.2
);
font-size
:
9px
;
font-weight
:
700
;
line-height
:
1
;
white-space
:
nowrap
;
opacity
:
0
;
pointer-events
:
none
;
transform
:
translate
(
4px
,
-50%
);
transition
:
opacity
.14s
ease
,
transform
.14s
ease
}
.rail
button
[
data-tooltip
]
:hover::after
,
.rail
button
[
data-tooltip
]
:focus-visible::after
{
opacity
:
1
;
transform
:
translate
(
0
,
-50%
)}
.rail
.rail-create-top
{
color
:
var
(
--muted
)
!important
;
background
:
transparent
!important
;
box-shadow
:
none
!important
}
.rail
.rail-create-top
:hover
,
.rail
.rail-create-top.active
{
color
:
var
(
--primary
)
!important
;
background
:
var
(
--soft
)
!important
}
.rail
.rail-create-top.active
{
box-shadow
:
inset
0
0
0
1px
#e5ddff
!important
}
@media
(
max-width
:
1100px
){
.create-layout
{
grid-template-columns
:
1
fr
;
gap
:
48px
;
padding-top
:
40px
}
.create-intro
{
text-align
:
center
}
.create-intro
>
p
,
.create-intro
h1
{
margin-left
:
auto
;
margin-right
:
auto
}
.intro-tag
{
margin
:
auto
}
.feature-row
{
max-width
:
620px
;
margin-left
:
auto
;
margin-right
:
auto
}
.create-card-wrap
{
justify-content
:
center
}
.workspace
{
grid-template-columns
:
58px
340px
1
fr
}
.create-workspace
{
grid-template-columns
:
58px
280px
1
fr
}
.workspace-create-content
{
padding
:
28px
}}
@media
(
max-width
:
1250px
)
and
(
min-width
:
801px
){
.preview-toolbar
{
height
:
96px
;
grid-template-columns
:
minmax
(
160px
,
1
fr
)
auto
;
grid-template-rows
:
42px
42px
;
grid-template-areas
:
"title actions"
"modes modes"
;
gap
:
0
10px
;
padding
:
6px
12px
}
.preview-modes
{
justify-content
:
center
}
.toolbar-actions
.environment-link
{
width
:
31px
;
padding
:
0
}
.toolbar-actions
.environment-link
>
span
{
display
:
none
}}
@media
(
max-width
:
1250px
)
and
(
min-width
:
801px
){
.preview-toolbar
{
height
:
96px
;
grid-template-columns
:
minmax
(
160px
,
1
fr
)
auto
;
grid-template-rows
:
42px
42px
;
grid-template-areas
:
"title actions"
"modes modes"
;
gap
:
0
10px
;
padding
:
6px
12px
}
.preview-modes
{
justify-content
:
center
}
.toolbar-actions
.environment-link
{
width
:
31px
;
padding
:
0
}
.toolbar-actions
.environment-link
>
span
{
display
:
none
}}
@media
(
max-width
:
800px
){
.landing-header
,
.create-layout
{
width
:
min
(
100%
-
30px
,
700px
)}
.landing-status
{
display
:
none
}
.feature-row
{
grid-template-columns
:
1
fr
}
.create-card
{
padding
:
26px
20px
}
.form-grid
{
grid-template-columns
:
1
fr
}
.field.wide
{
grid-column
:
auto
}
.workspace
{
grid-template-columns
:
52px
1
fr
}
.control-panel
{
border-right
:
0
}
.preview-shell
{
display
:
none
}
.create-intro
h1
{
font-size
:
48px
}
.create-layout
{
padding-bottom
:
35px
}}
@media
(
max-width
:
800px
){
.landing-header
,
.create-layout
{
width
:
min
(
100%
-
30px
,
700px
)}
.landing-status
{
display
:
none
}
.feature-row
{
grid-template-columns
:
1
fr
}
.create-card
{
padding
:
26px
20px
}
.form-grid
{
grid-template-columns
:
1
fr
}
.field.wide
{
grid-column
:
auto
}
.workspace
{
grid-template-columns
:
52px
1
fr
}
.control-panel
{
border-right
:
0
}
.preview-shell
{
display
:
none
}
.create-intro
h1
{
font-size
:
48px
}
.create-layout
{
padding-bottom
:
35px
}
.create-workspace
{
grid-template-columns
:
52px
1
fr
}
.create-workspace
.create-control-panel
{
display
:
none
}
.workspace-create-content
{
padding
:
18px
}
.login-card
{
padding
:
30px
24px
}
}
shared/src/index.ts
View file @
f86dd1a6
...
@@ -24,6 +24,9 @@ export interface SiteInfo {
...
@@ -24,6 +24,9 @@ export interface SiteInfo {
currentCommit
:
string
;
currentCommit
:
string
;
previewCommit
?:
string
;
previewCommit
?:
string
;
previousCommit
?:
string
;
previousCommit
?:
string
;
draftBaseCommit
?:
string
;
draftUpdatedAt
?:
string
;
draftSummary
?:
string
;
publishedCommit
?:
string
;
publishedCommit
?:
string
;
publishedAt
?:
string
;
publishedAt
?:
string
;
publishStatus
:
PublishStatus
;
publishStatus
:
PublishStatus
;
...
@@ -52,8 +55,9 @@ export interface GitHistoryItem {
...
@@ -52,8 +55,9 @@ export interface GitHistoryItem {
export
interface
ChatResult
{
export
interface
ChatResult
{
summary
:
string
;
summary
:
string
;
commit
:
string
;
draft
:
true
;
previewUrl
:
string
;
baseCommit
:
string
;
updatedAt
:
string
;
changedFiles
:
string
[];
changedFiles
:
string
[];
mode
:
"model"
|
"local"
;
mode
:
"model"
|
"local"
;
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment