470 lines
20 KiB
Python
470 lines
20 KiB
Python
import re
|
|
|
|
import gradio as gr
|
|
import modelscope_studio.components.antd as antd
|
|
import modelscope_studio.components.base as ms
|
|
import modelscope_studio.components.pro as pro
|
|
from openai import OpenAI
|
|
from config import API_KEY, MODEL, SYSTEM_PROMPT, ENDPOINT, EXAMPLES, DEFAULT_LOCALE, DEFAULT_THEME
|
|
|
|
client = OpenAI(api_key=API_KEY, base_url=ENDPOINT)
|
|
|
|
react_imports = {
|
|
"lucide-react": "https://esm.sh/lucide-react@0.525.0",
|
|
"recharts": "https://esm.sh/recharts@3.1.0",
|
|
"framer-motion": "https://esm.sh/framer-motion@12.23.6",
|
|
"matter-js": "https://esm.sh/matter-js@0.20.0",
|
|
"p5": "https://esm.sh/p5@2.0.3",
|
|
"konva": "https://esm.sh/konva@9.3.22",
|
|
"react-konva": "https://esm.sh/react-konva@19.0.7",
|
|
"three": "https://esm.sh/three@0.178.0",
|
|
"@react-three/fiber": "https://esm.sh/@react-three/fiber@9.2.0",
|
|
"@react-three/drei": "https://esm.sh/@react-three/drei@10.5.2",
|
|
"@tailwindcss/browser": "https://esm.sh/@tailwindcss/browser@4.1.11",
|
|
"react": "https://esm.sh/react@^19.0.0",
|
|
"react/": "https://esm.sh/react@^19.0.0/",
|
|
"react-dom": "https://esm.sh/react-dom@^19.0.0",
|
|
"react-dom/": "https://esm.sh/react-dom@^19.0.0/"
|
|
}
|
|
|
|
|
|
class GradioEvents:
|
|
|
|
@staticmethod
|
|
def generate_code(input_value, system_prompt_input_value, state_value):
|
|
|
|
def get_generated_files(text):
|
|
patterns = {
|
|
'html': r'```html\n(.+?)\n```',
|
|
'jsx': r'```jsx\n(.+?)\n```',
|
|
'tsx': r'```tsx\n(.+?)\n```',
|
|
}
|
|
result = {}
|
|
|
|
for ext, pattern in patterns.items():
|
|
matches = re.findall(pattern, text, re.DOTALL)
|
|
if matches:
|
|
content = '\n'.join(matches).strip()
|
|
result[f'index.{ext}'] = content
|
|
|
|
if len(result) == 0:
|
|
result["index.html"] = text.strip()
|
|
return result
|
|
|
|
yield {
|
|
output_loading: gr.update(spinning=True),
|
|
state_tab: gr.update(active_key="loading"),
|
|
output: gr.update(value=None)
|
|
}
|
|
|
|
if input_value is None:
|
|
input_value = ''
|
|
|
|
# Keep only conversational turns in history; inject a single system
|
|
# prompt at request time so providers that require strict ordering work.
|
|
history = [
|
|
message for message in state_value["history"]
|
|
if message.get("role") in ("user", "assistant")
|
|
]
|
|
messages = [{
|
|
'role': "system",
|
|
"content": SYSTEM_PROMPT
|
|
# 'content': system_prompt_input_value
|
|
}] + history
|
|
|
|
current_user_message = {'role': "user", 'content': input_value}
|
|
messages.append(current_user_message)
|
|
|
|
generator = client.chat.completions.create(
|
|
model=MODEL,
|
|
messages=messages,
|
|
stream=True,
|
|
extra_body={"enable_thinking": False},
|
|
)
|
|
response = ""
|
|
for chunk in generator:
|
|
choice = chunk.choices[0]
|
|
content = choice.delta.content or ""
|
|
if content:
|
|
response += content
|
|
if choice.finish_reason == 'stop':
|
|
state_value["history"] = history + [current_user_message, {
|
|
'role': "assistant",
|
|
'content': response
|
|
}]
|
|
generated_files = get_generated_files(response)
|
|
react_code = generated_files.get(
|
|
"index.tsx") or generated_files.get("index.jsx")
|
|
html_code = generated_files.get("index.html")
|
|
# Completed
|
|
yield {
|
|
output:
|
|
gr.update(value=response),
|
|
download_content:
|
|
gr.update(value=react_code or html_code),
|
|
state_tab:
|
|
gr.update(active_key="render"),
|
|
output_loading:
|
|
gr.update(spinning=False),
|
|
sandbox:
|
|
gr.update(
|
|
template="react" if react_code else "html",
|
|
imports=react_imports if react_code else {},
|
|
value={
|
|
"./index.tsx": """import Demo from './demo.tsx'
|
|
import "@tailwindcss/browser"
|
|
|
|
export default Demo
|
|
""",
|
|
"./demo.tsx": react_code
|
|
} if react_code else {"./index.html": html_code}),
|
|
state:
|
|
gr.update(value=state_value)
|
|
}
|
|
|
|
else:
|
|
# Generating
|
|
yield {
|
|
output: gr.update(value=response),
|
|
output_loading: gr.update(spinning=False),
|
|
}
|
|
|
|
@staticmethod
|
|
def select_example(example: dict):
|
|
return lambda: gr.update(value=example["description"])
|
|
|
|
@staticmethod
|
|
def close_modal():
|
|
return gr.update(open=False)
|
|
|
|
@staticmethod
|
|
def open_modal():
|
|
return gr.update(open=True)
|
|
|
|
@staticmethod
|
|
def disable_btns(btns: list):
|
|
return lambda: [gr.update(disabled=True) for _ in btns]
|
|
|
|
@staticmethod
|
|
def enable_btns(btns: list):
|
|
return lambda: [gr.update(disabled=False) for _ in btns]
|
|
|
|
@staticmethod
|
|
def update_system_prompt(system_prompt_input_value, state_value):
|
|
state_value["system_prompt"] = system_prompt_input_value
|
|
return gr.update(value=state_value)
|
|
|
|
@staticmethod
|
|
def reset_system_prompt(state_value):
|
|
return gr.update(value=state_value["system_prompt"])
|
|
|
|
@staticmethod
|
|
def render_history(statue_value):
|
|
return gr.update(value=statue_value["history"])
|
|
|
|
@staticmethod
|
|
def clear_history(state_value):
|
|
gr.Success("历史记录已清空。")
|
|
state_value["history"] = []
|
|
return gr.update(value=state_value)
|
|
|
|
|
|
css = """
|
|
#coder-artifacts .output-empty,.output-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
min-height: 680px;
|
|
}
|
|
|
|
#coder-artifacts #output-container .ms-gr-ant-tabs-content,.ms-gr-ant-tabs-tabpane {
|
|
height: 100%;
|
|
}
|
|
|
|
#coder-artifacts .output-html {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
min-height: 680px;
|
|
max-height: 1200px;
|
|
}
|
|
|
|
#coder-artifacts .output-html > iframe {
|
|
flex: 1;
|
|
}
|
|
|
|
#coder-artifacts-code-drawer .output-code {
|
|
flex:1;
|
|
}
|
|
#coder-artifacts-code-drawer .output-code .ms-gr-ant-spin-nested-loading {
|
|
min-height: 100%;
|
|
}
|
|
|
|
"""
|
|
|
|
with gr.Blocks(css=css) as demo:
|
|
# Global State
|
|
state = gr.State({"system_prompt": "", "history": []})
|
|
with ms.Application(elem_id="coder-artifacts") as app:
|
|
with antd.ConfigProvider(theme=DEFAULT_THEME, locale=DEFAULT_LOCALE):
|
|
|
|
with ms.AutoLoading():
|
|
with antd.Row(gutter=[32, 12],
|
|
elem_style=dict(marginTop=20),
|
|
align="stretch"):
|
|
# Left Column
|
|
with antd.Col(span=24, md=8):
|
|
with antd.Flex(vertical=True, gap="middle", wrap=True):
|
|
with antd.Flex(justify="center",
|
|
align="center",
|
|
vertical=True,
|
|
gap="middle"):
|
|
antd.Image(
|
|
"logo.png",
|
|
width=200,
|
|
preview=False)
|
|
antd.Typography.Title(
|
|
"网页应用生成器",
|
|
level=1,
|
|
elem_style=dict(fontSize=24))
|
|
# Input
|
|
input = antd.Input.Textarea(
|
|
size="large",
|
|
allow_clear=True,
|
|
auto_size=dict(minRows=2, maxRows=6),
|
|
placeholder=
|
|
"请描述你想生成的网页应用",
|
|
elem_id="input-container")
|
|
# Input Notes
|
|
with antd.Flex(justify="space-between"):
|
|
antd.Typography.Text(
|
|
"说明:模型支持多轮对话,可通过返回 React 或 HTML 代码生成界面。",
|
|
strong=True,
|
|
type="warning")
|
|
|
|
tour_btn = antd.Button("使用引导",
|
|
variant="filled",
|
|
color="default")
|
|
# Submit Button
|
|
submit_btn = antd.Button("提交",
|
|
type="primary",
|
|
block=True,
|
|
size="large",
|
|
elem_id="submit-btn")
|
|
|
|
antd.Divider("设置")
|
|
|
|
# Settings Area
|
|
with antd.Space(size="small",
|
|
wrap=True,
|
|
elem_id="settings-area"):
|
|
# system_prompt_btn = antd.Button(
|
|
# "⚙️ Set System Prompt", type="default")
|
|
history_btn = antd.Button(
|
|
"📜 历史记录",
|
|
type="default",
|
|
elem_id="history-btn",
|
|
)
|
|
cleat_history_btn = antd.Button(
|
|
"🧹 清空历史", danger=True)
|
|
|
|
antd.Divider("示例")
|
|
|
|
# Examples
|
|
with antd.Flex(gap="small", wrap=True):
|
|
for example in EXAMPLES:
|
|
with antd.Card(
|
|
elem_style=dict(
|
|
flex="1 1 fit-content"),
|
|
hoverable=True) as example_card:
|
|
antd.Card.Meta(
|
|
title=example['title'],
|
|
description=example['description'])
|
|
|
|
example_card.click(
|
|
fn=GradioEvents.select_example(
|
|
example),
|
|
outputs=[input])
|
|
|
|
# Right Column
|
|
with antd.Col(span=24, md=16):
|
|
with antd.Card(
|
|
title="输出",
|
|
elem_style=dict(height="100%",
|
|
display="flex",
|
|
flexDirection="column"),
|
|
styles=dict(body=dict(height=0, flex=1)),
|
|
elem_id="output-container"):
|
|
# Output Container Extra
|
|
with ms.Slot("extra"):
|
|
with ms.Div(elem_id="output-container-extra"):
|
|
with antd.Button(
|
|
"下载代码",
|
|
type="link",
|
|
href_target="_blank",
|
|
disabled=True,
|
|
) as download_btn:
|
|
with ms.Slot("icon"):
|
|
antd.Icon("DownloadOutlined")
|
|
download_content = gr.Text(visible=False)
|
|
|
|
view_code_btn = antd.Button(
|
|
"🧑💻 查看代码", type="primary")
|
|
# Output Content
|
|
with antd.Tabs(
|
|
elem_style=dict(height="100%"),
|
|
active_key="empty",
|
|
render_tab_bar="() => null") as state_tab:
|
|
with antd.Tabs.Item(key="empty"):
|
|
antd.Empty(
|
|
description=
|
|
"输入你的需求以生成代码",
|
|
elem_classes="output-empty")
|
|
with antd.Tabs.Item(key="loading"):
|
|
with antd.Spin(
|
|
tip="正在生成代码...",
|
|
size="large",
|
|
elem_classes="output-loading"):
|
|
# placeholder
|
|
ms.Div()
|
|
with antd.Tabs.Item(key="render"):
|
|
sandbox = pro.WebSandbox(
|
|
height="100%",
|
|
elem_classes="output-html",
|
|
template="html",
|
|
)
|
|
|
|
# Modals and Drawers
|
|
with antd.Modal(open=False,
|
|
title="系统提示词",
|
|
width="800px") as system_prompt_modal:
|
|
system_prompt_input = antd.Input.Textarea(
|
|
# SYSTEM_PROMPT,
|
|
value="",
|
|
size="large",
|
|
placeholder="请输入系统提示词",
|
|
allow_clear=True,
|
|
auto_size=dict(minRows=4, maxRows=14))
|
|
|
|
with antd.Drawer(
|
|
open=False,
|
|
title="输出代码",
|
|
placement="right",
|
|
get_container=
|
|
"() => document.querySelector('.gradio-container')",
|
|
elem_id="coder-artifacts-code-drawer",
|
|
styles=dict(
|
|
body=dict(display="flex",
|
|
flexDirection="column-reverse")),
|
|
width="750px") as output_code_drawer:
|
|
with ms.Div(elem_classes="output-code"):
|
|
with antd.Spin(spinning=False) as output_loading:
|
|
output = ms.Markdown()
|
|
|
|
with antd.Drawer(
|
|
open=False,
|
|
title="对话历史",
|
|
placement="left",
|
|
get_container=
|
|
"() => document.querySelector('.gradio-container')",
|
|
width="750px") as history_drawer:
|
|
history_output = gr.Chatbot(
|
|
show_label=False,
|
|
type="messages",
|
|
height='100%',
|
|
elem_classes="history_chatbot")
|
|
# Tour
|
|
with antd.Tour(open=False) as usage_tour:
|
|
antd.Tour.Step(
|
|
title="第 1 步",
|
|
description=
|
|
"描述你想创建的网页应用。",
|
|
get_target=
|
|
"() => document.querySelector('#input-container')")
|
|
antd.Tour.Step(
|
|
title="第 2 步",
|
|
description="点击提交按钮。",
|
|
get_target=
|
|
"() => document.querySelector('#submit-btn')")
|
|
antd.Tour.Step(
|
|
title="第 3 步",
|
|
description="等待生成结果。",
|
|
get_target=
|
|
"() => document.querySelector('#output-container')"
|
|
)
|
|
antd.Tour.Step(
|
|
title="第 4 步",
|
|
description=
|
|
"可在这里下载生成代码或查看代码内容。",
|
|
get_target=
|
|
"() => document.querySelector('#output-container-extra')"
|
|
)
|
|
antd.Tour.Step(
|
|
title="其他设置",
|
|
description="你可以在这里查看和管理对话历史。",
|
|
get_target=
|
|
"() => document.querySelector('#settings-area')")
|
|
# Event Handler
|
|
gr.on(fn=GradioEvents.close_modal,
|
|
triggers=[usage_tour.close, usage_tour.finish],
|
|
outputs=[usage_tour])
|
|
tour_btn.click(fn=GradioEvents.open_modal, outputs=[usage_tour])
|
|
|
|
# system_prompt_btn.click(fn=GradioEvents.open_modal,
|
|
# outputs=[system_prompt_modal])
|
|
|
|
system_prompt_modal.ok(GradioEvents.update_system_prompt,
|
|
inputs=[system_prompt_input, state],
|
|
outputs=[state]).then(fn=GradioEvents.close_modal,
|
|
outputs=[system_prompt_modal])
|
|
|
|
system_prompt_modal.cancel(GradioEvents.close_modal,
|
|
outputs=[system_prompt_modal]).then(
|
|
fn=GradioEvents.reset_system_prompt,
|
|
inputs=[state],
|
|
outputs=[system_prompt_input])
|
|
output_code_drawer.close(fn=GradioEvents.close_modal,
|
|
outputs=[output_code_drawer])
|
|
cleat_history_btn.click(fn=GradioEvents.clear_history,
|
|
inputs=[state],
|
|
outputs=[state])
|
|
history_btn.click(fn=GradioEvents.open_modal,
|
|
outputs=[history_drawer
|
|
]).then(fn=GradioEvents.render_history,
|
|
inputs=[state],
|
|
outputs=[history_output])
|
|
history_drawer.close(fn=GradioEvents.close_modal, outputs=[history_drawer])
|
|
|
|
download_btn.click(fn=None,
|
|
inputs=[download_content],
|
|
js="""(content) => {
|
|
const blob = new Blob([content], { type: 'text/plain' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = 'output.txt'
|
|
a.click()
|
|
}""")
|
|
view_code_btn.click(fn=GradioEvents.open_modal,
|
|
outputs=[output_code_drawer])
|
|
submit_btn.click(
|
|
fn=GradioEvents.open_modal,
|
|
outputs=[output_code_drawer],
|
|
).then(fn=GradioEvents.disable_btns([submit_btn, download_btn]),
|
|
outputs=[submit_btn, download_btn]).then(
|
|
fn=GradioEvents.generate_code,
|
|
inputs=[input, system_prompt_input, state],
|
|
outputs=[
|
|
output, state_tab, sandbox, download_content,
|
|
output_loading, state
|
|
]).then(fn=GradioEvents.enable_btns([submit_btn, download_btn]),
|
|
outputs=[submit_btn, download_btn
|
|
]).then(fn=GradioEvents.close_modal,
|
|
outputs=[output_code_drawer])
|
|
|
|
if __name__ == "__main__":
|
|
demo.queue(default_concurrency_limit=100,
|
|
max_size=100).launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False, max_threads=100)
|