AI UI 生成器 - 简化版 Google Stitch
学在坚持公众号
简介
基于大模型(Qwen3.5-397B)实现的文本转UI工具。输入自然语言描述,AI自动生成完整的HTML页面代码,并在浏览器中实时预览。支持对话式迭代修改,像和设计师对话一样调整界面。
核心原理
用户输入文字描述 ↓System Prompt(约束输出格式)+ 用户Prompt ↓调用 Qwen3.5-397B API ↓返回完整HTML代码 ↓保存为临时文件 → 浏览器打开预览 ↓用户提出修改 → 追加到对话历史 → 再次调用API → 更新预览
环境要求
pip install requests
运行方式
cd python/ngorkpython ai_ui_generator.py
功能说明
| |
|---|
| |
| 一键填入常用场景(登录页/仪表盘/商品卡片/简历) |
| |
| |
| |
| |
界面布局
┌──────────────────────────────────────────────────────┐│ AI UI 生成器 │├────────────────────────┬─────────────────────────────┤│ 描述你想要的UI │ ││ ┌──────────────────┐ │ 实时预览 ││ │ 输入框(多行) │ │ ││ └──────────────────┘ │ (自动在浏览器中打开) ││ │ ││ [登录页][仪表盘]... │ ││ │ ││ [生成UI][修改当前] │ 预览信息 ││ [浏览器预览][导出] │ ✅ 代码已生成 ││ │ 💡 输入修改要求可迭代 ││ 生成的代码 │ ││ ┌──────────────────┐ │ ││ │ <!DOCTYPE html> │ │ ││ │ <html>... │ │ ││ │ (深色代码区) │ │ ││ └──────────────────┘ │ │└────────────────────────┴─────────────────────────────┘
测试步骤
测试1:基本生成
- 启动程序:
python ai_ui_generator.py - 在输入框中输入:
一个现代风格的登录页面,有用户名和密码输入框,登录按钮
测试2:快捷模板
- 预期结果:生成一个带导航栏、统计卡片、表格的仪表盘页面
测试3:对话式修改
- 在输入框中输入:
把背景改成蓝紫渐变色,按钮改成圆角
测试4:连续迭代
- 接测试3,继续输入:
增加一个"忘记密码"链接和第三方登录按钮(微信、QQ) - 预期结果:在原有基础上增加了新元素,之前的渐变背景和圆角按钮保持不变
测试5:导出
测试6:错误处理
代码架构
ai_ui_generator.py├── API_CONFIG # API配置(endpoint/key/model)├── SYSTEM_PROMPT # 系统提示词(约束输出格式)└── AIUIGenerator # 主类 ├── _create_ui() # 构建GUI界面 ├── _generate() # 新建生成(清空历史) ├── _modify() # 迭代修改(追加历史) ├── _call_api() # 调用LLM API(子线程) ├── _extract_html() # 从响应中提取HTML代码 ├── _on_success() # 生成成功回调 ├── _on_error() # 错误处理 ├── _save_and_preview() # 保存临时文件+浏览器打开 └── _export() # 导出HTML文件
关键设计
System Prompt 设计
你是一个专业的前端UI设计师和开发者。根据用户的描述生成完整的HTML页面代码。规则:1. 生成完整的HTML文件(包含<!DOCTYPE html>、<head>、<body>)2. 使用内联CSS或<style>标签,不依赖外部文件3. 设计要现代、美观、响应式4. 只输出HTML代码,不要任何解释文字5. 不要用```html```包裹,直接输出代码6. 中文界面优先使用 -apple-system, 'Microsoft YaHei' 字体7. 配色要协调,间距要合理
这个Prompt的关键约束:
- "完整HTML文件":确保生成的代码可以直接在浏览器运行
API调用示例
import requestsresp = requests.post("https://xxxx/shop/v1/chat/completions", headers={"Authorization": "Bearer 4wWwsCTKCn4wtctz","Content-Type": "application/json" }, json={"model": "qwen3.5-397b","messages": [ {"role": "system", "content": "你是前端专家,只输出HTML代码..."}, {"role": "user", "content": "生成一个登录页面"} ],"temperature": 0.7,"max_tokens": 4000 }, timeout=90)html = resp.json()['choices'][0]['message']['content']
对话式修改原理
# 第一次生成history = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": "生成登录页面"},]# API返回HTML → 追加到historyhistory.append({"role": "assistant", "content": "<html>..."})# 第二次修改history.append({"role": "user", "content": "把背景改成蓝色"})# API看到完整对话历史,知道当前代码是什么,只修改背景色# 返回修改后的完整HTML
关键:每次修改都带上完整对话历史,AI能"记住"当前代码状态,做增量修改而不是重新生成。
常见问题
Q: 生成速度慢?
A: 397B参数模型推理较慢,通常需要10-30秒。这是正常的。
Q: 生成的代码不完整?
A: max_tokens 设为4000,复杂页面可能被截断。可以在代码中调大到8000。
Q: 修改时AI重新生成了整个页面?
A: 在修改提示中明确说"只修改xxx,其他保持不变"效果更好。
Q: 预览页面空白?
A: 检查代码区是否有有效HTML。如果API返回了非HTML内容,_extract_html() 会尝试修复。
Q: 能生成React/Vue代码吗?
A: 可以在输入中指定,如"用React组件方式生成",但预览需要额外构建步骤。
与Google Stitch对比
后续可扩展
- [ ] 支持指定CSS框架(Tailwind/Bootstrap)
- [ ] 内嵌WebView预览(不依赖外部浏览器)
"""ChatFlask - AI对话Web应用Flask后端:API代理(解决CORS)+ 流式输出(SSE)+ 页面路由"""from flask import Flask, request, Response, render_template, jsonify, stream_with_contextimport requestsimport jsonimport timeimport uuidapp = Flask(name)内存存储对话历史conversations = {} # {conv_id: {"title": str, "messages": [], "created_at": float}}@app.route('/')def index():return render_template('index.html')@app.route('/api/chat', methods=['POST'])def chat():"""API代理 - 流式输出"""data = request.jsonendpoint = data.get('endpoint', '')api_key = data.get('api_key', 'sk-')model = data.get('model', 'qwen-plus')messages = data.get('messages', [])temperature = data.get('temperature', 0.7)max_tokens = data.get('max_tokens', 4000)top_p = data.get('top_p', 0.9)stream = data.get('stream', True)if not endpoint or not api_key: return jsonify({'error': '请配置API地址和Key'}), 400headers = { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json',}payload = { 'model': model, 'messages': messages, 'temperature': temperature, 'max_tokens': max_tokens, 'top_p': top_p, 'stream': stream,}if stream: return Response( stream_with_context(_stream_response(endpoint, headers, payload)), content_type='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'} )else: try: resp = requests.post(endpoint, headers=headers, json=payload, timeout=90) if resp.status_code != 200: return jsonify({'error': f'API错误: {resp.status_code}{resp.text[:200]}'}), resp.status_code result = resp.json() content = result['choices'][0]['message']['content'] return jsonify({'content': content}) except requests.Timeout: return jsonify({'error': '请求超时'}), 504 except Exception as e: return jsonify({'error': str(e)}), 500def _stream_response(endpoint, headers, payload):"""流式代理转发"""try:resp = requests.post(endpoint, headers=headers, json=payload, stream=True, timeout=90)if resp.status_code != 200:yield f"data: {json.dumps({'error': f'API错误: {resp.status_code}'})}\n\n"return for line in resp.iter_lines(): if line: line_str = line.decode('utf-8') if line_str.startswith('data: '): data_str = line_str[6:] if data_str.strip() == '[DONE]': yield f"data: {json.dumps({'content': '', 'done': True})}\n\n" break try: chunk = json.loads(data_str) delta = chunk.get('choices', [{}])[0].get('delta', {}) content = delta.get('content', '') if content: yield f"data: {json.dumps({'content': content, 'done': False})}\n\n" except json.JSONDecodeError: continueexcept requests.Timeout: yield f"data: {json.dumps({'error': '请求超时'})}\n\n"except Exception as e: yield f"data: {json.dumps({'error': str(e)})}\n\n"@app.route('/api/chat/sync', methods=['POST'])def chat_sync():"""非流式调用(备用)"""data = request.jsonendpoint = data.get('endpoint', '')api_key = data.get('api_key', '')headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}payload = { 'model': data.get('model', 'qwen-plus'), 'messages': data.get('messages', []), 'temperature': data.get('temperature', 0.7), 'max_tokens': data.get('max_tokens', 4000), 'top_p': data.get('top_p', 0.9), 'stream': False,}try: resp = requests.post(endpoint, headers=headers, json=payload, timeout=90) result = resp.json() content = result.get('choices', [{}])[0].get('message', {}).get('content', '') # 有些模型把内容放在reasoning_content里 if not content: content = result.get('choices', [{}])[0].get('message', {}).get('reasoning_content', '') return jsonify({'content': content})except Exception as e: return jsonify({'error': str(e)}), 500=== 对话历史管理 ===@app.route('/api/conversations', methods=['GET'])def list_conversations():conv_list = [{'id': k, 'title': v['title'], 'created_at': v['created_at']}for k, v in conversations.items()]conv_list.sort(key=lambda x: x['created_at'], reverse=True)return jsonify(conv_list)@app.route('/api/conversations', methods=['POST'])def create_conversation():conv_id = str(uuid.uuid4())[:8]conversations[conv_id] = {'title': '新对话', 'messages': [], 'created_at': time.time()}return jsonify({'id': conv_id})@app.route('/api/conversations/<conv_id>', methods=['GET'])def get_conversation(conv_id):if conv_id in conversations:return jsonify(conversations[conv_id])return jsonify({'error': '对话不存在'}), 404@app.route('/api/conversations/<conv_id>', methods=['PUT'])def update_conversation(conv_id):if conv_id not in conversations:return jsonify({'error': '对话不存在'}), 404data = request.jsonif 'title' in data:conversations[conv_id]['title'] = data['title']if 'messages' in data:conversations[conv_id]['messages'] = data['messages']return jsonify({'status': 'ok'})@app.route('/api/conversations/<conv_id>', methods=['DELETE'])def delete_conversation(conv_id):if conv_id in conversations:del conversations[conv_id]return jsonify({'status': 'ok'})if name == 'main':app.run(host='0.0.0.0', port=5000, debug=True)