Bolt Prompt Selector

Add prompt selector for bolt.new

// ==UserScript==
// @name         Bolt Prompt Selector
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Add prompt selector for bolt.new
// @author       Your name
// @match        https://bolt.new/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    function init() {
        // 清理已存在的元素
        const cleanup = () => {
            const existingTooltip = document.querySelector('.prompt-tooltip');
            if (existingTooltip) {
                existingTooltip.remove();
            }
        };
        cleanup();

        // 检查是否已注入
        if (document.querySelector('.prompt-container')) {
            return;
        }

        // 检查必要的DOM元素
        const targetElement = document.querySelector('.z-prompt');
        if (!targetElement) {
            console.log('Target element not found, waiting...');
            return;
        }

        const textarea = document.querySelector('textarea.w-full');
        if (!textarea) {
            console.log('Textarea not found, waiting...');
            return;
        }

        const promptList = [
        {
            title: "DRY原则和数据视图分离",
            prompt: "遵循DRY(Don't Repeat Yourself)原则,将数据逻辑与视图逻辑分离。使用专门的数据层处理状态和业务逻辑,通过props或context传递给视图组件。",
            group: "Architecture"
        },
        {
            title: "类型定义优先",
            prompt: "在实现具体功能前,优先定义完整的TypeScript类型和接口。包括API响应类型、组件Props类型、状态类型等。",
            group: "Architecture"
        },
        {
            title: "模块化开发",
            prompt: "将代码按功能模块化,每个模块应该是独立的、可测试的单元。",
            group: "Architecture"
        },
        {
            title: "Shadcn UI优先",
            prompt: "优先使用Shadcn UI组件库构建界面。避免重复造轮子。",
            group: "UI"
        },
        {
            title: "响应式设计规范",
            prompt: "使用Tailwind的响应式前缀(sm:、md:、lg:)构建响应式布局。移动端优先,确保在各种设备上都有良好的显示效果。",
            group: "UI"
        },
        {
            title: "主题定制规范",
            prompt: "在globals.css中集中管理主题变量。使用Shadcn UI的主题系统,通过CSS变量统一管理颜色、字体等样式。",
            group: "UI"
        },
        {
            title: "表单字段验证",
            prompt: "使用Zod进行表单验证,为每个表单创建专门的验证schema。必填字段使用.required(),可选字段使用.optional()。错误信息应该明确友好。",
            group: "Form"
        },
        {
            title: "表单布局规范",
            prompt: "表单采用紧凑(compact)布局,简单input字段同行显示,最多两列。较复杂的字段独占一行。",
            group: "Form"
        },
        {
            title: "表单状态管理",
            prompt: "使用React Hook Form管理表单状态。实现实时验证、提交处理、错误展示等完整表单生命周期。",
            group: "Form"
        },
        {
            title: "Server Components优先",
            prompt: "优先使用Server Components处理数据获取。合理使用use server和use client指令。避免不必要的客户端渲染。",
            group: "Data"
        },
        {
            title: "缓存策略",
            prompt: "根据数据特点设置合适的缓存策略。使用Next.js的缓存API。实现适当的重验证机制。",
            group: "Data"
        },
        {
            title: "元数据管理",
            prompt: "使用Next.js的Metadata API管理SEO元数据。在layout.tsx和page.tsx中设置动态metadata。确保title、description等关键标签完整。",
            group: "SEO"
        },
        {
            title: "结构化数据",
            prompt: "实现JSON-LD结构化数据。针对不同类型的页面(文章、商品、评论等)使用对应的Schema标记。验证结构化数据的正确性。",
            group: "SEO"
        },
        {
            title: "语义化HTML",
            prompt: "使用语义化的HTML标签构建页面结构。合理使用heading层级(h1-h6)。使用article、section、nav等语义化标签标识内容区域。",
            group: "SEO"
        },
        {
            title: "图片优化SEO",
            prompt: "图片添加有意义的alt文本。使用Next.js的Image组件自动优化图片。考虑使用srcset提供响应式图片。添加图片的width和height避免布局偏移。",
            group: "SEO"
        },
        {
            title: "性能优化SEO",
            prompt: "确保核心Web指标(Core Web Vitals)达标。包括LCP、FID、CLS等指标的优化。使用next/dynamic实现代码分割。",
            group: "SEO"
        },
        {
            title: "国际化SEO",
            prompt: "使用i18next实现多语言支持,语言文件存储在app/i18n/locales文件夹中。添加合适的hreflang标签。考虑内容的本地化需求。",
            group: "SEO"
        },
        {
            title: "统一错误处理",
            prompt: "实现统一的错误边界处理组件。API调用使用try-catch包装,展示友好的错误提示。记录错误日志便于追踪。",
            group: "Error"
        },
        {
            title: "加载状态处理",
            prompt: "实现统一的加载状态组件。使用Suspense和loading.tsx处理路由切换。接口调用时展示适当的加载提示。",
            group: "Error"
        },
        {
            title: "图片优化",
            prompt: "使用Next.js的Image组件处理图片。设置合适的宽高、质量参数。使用BlurHash等技术优化加载体验。",
            group: "Performance"
        },
        {
            title: "状态管理优化",
            prompt: "合理使用React状态管理。局部状态用useState,跨组件状态用Context,复杂状态考虑使用Zustand等轻量级方案。",
            group: "Performance"
        },
        {
            title: "Code Splitting",
            prompt: "使用Next.js的动态导入功能(dynamic import)实现代码分割。路由级别的组件默认分割,大型组件或库考虑按需加载。",
            group: "Performance"
        }
    ].map(item => ({ ...item, selected: false }));

        // 创建容器
        const container = document.createElement('div');
        container.classList.add('prompt-container');
        container.style.cssText = 'width:502px;max-height:300px;overflow-y:auto;background:#1a1a1a;padding:8px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.2);font-family:system-ui,-apple-system;scrollbar-width:thin;scrollbar-color:#4a4a4a #1a1a1a;opacity:0;transform:translateY(-10px);transition:all 0.2s ease;display:none;';
        container.onscroll = (e) => e.stopPropagation();

        // 创建tooltip
        const tooltip = document.createElement('div');
        tooltip.classList.add('prompt-tooltip');
        tooltip.style.cssText = `
            position: fixed;
            padding: 8px 12px;
            background: #2d2d2d;
            border: 1px solid #404040;
            border-radius: 6px;
            font-size: 13px;
            color: #e5e7eb;
            max-width: 300px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            pointer-events: none;
            opacity: 0;
            transform: translateY(4px);
            transition: opacity 0.2s, transform 0.2s;
            z-index: 9999;
            line-height: 1.5;
            font-family: system-ui, -apple-system;
            word-break: break-word;
        `;
        document.body.appendChild(tooltip);

        // 辅助函数:设置textarea的值
        const setTextareaValue = (textarea, value) => {
            try {
                const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
                nativeInputValueSetter.call(textarea, value);
                const event = new Event('input', { bubbles: true });
                textarea.dispatchEvent(event);
            } catch (error) {
                console.error('Error setting textarea value:', error);
            }
        };

        // 按组分类
        const groups = promptList.reduce((acc, item) => {
            if (!acc[item.group]) {
                acc[item.group] = [];
            }
            acc[item.group].push(item);
            return acc;
        }, {});

        // 渲染按钮
        const renderButtons = () => {
            try {
                Object.entries(groups).forEach(([groupName, items]) => {
                    const groupDiv = document.createElement('div');
                    groupDiv.style.cssText = 'margin:4px 0;';

                    const groupLabel = document.createElement('div');
                    groupLabel.textContent = groupName;
                    groupLabel.style.cssText = 'color:#6b7280;font-size:12px;padding:0 4px;margin-bottom:2px;';
                    groupDiv.appendChild(groupLabel);

                    const buttonsContainer = document.createElement('div');
                    buttonsContainer.style.cssText = 'display:grid;grid-template-columns:repeat(2,1fr);gap:4px;';

                    items.forEach(item => {
                        const button = document.createElement('button');
                        button.textContent = item.title;

                        const getButtonStyles = (isSelected, isHovered = false) => `
                            display:block;
                            width:100%;
                            padding:6px 10px;
                            border:1px solid ${isSelected ? '#4CAF50' : '#333'};
                            border-radius:4px;
                            background:${isSelected ? '#1b4a1f' : isHovered ? '#2f2f2f' : '#242424'};
                            color:${isSelected ? '#4CAF50' : '#e5e7eb'};
                            font-size:13px;
                            text-align:left;
                            cursor:${isSelected ? 'not-allowed' : 'pointer'};
                            transition:all 0.2s;
                            white-space:nowrap;
                            overflow:hidden;
                            text-overflow:ellipsis;
                            opacity:${isSelected ? '0.8' : '1'};
                        `;

                        button.style.cssText = getButtonStyles(item.selected);

                        // Hover events for tooltip
                        button.addEventListener('mouseenter', (e) => {
                            const rect = e.target.getBoundingClientRect();
                            tooltip.textContent = item.prompt;
                            tooltip.style.opacity = '1';
                            tooltip.style.transform = 'translateY(0)';

                            // Calculate best position
                            const tooltipRect = tooltip.getBoundingClientRect();
                            const spaceBelow = window.innerHeight - rect.bottom;
                            const spaceAbove = rect.top;

                            if (spaceBelow >= tooltipRect.height + 8) {
                                tooltip.style.top = `${rect.bottom + 8}px`;
                            } else if (spaceAbove >= tooltipRect.height + 8) {
                                tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`;
                            } else {
                                tooltip.style.top = `${window.innerHeight/2 - tooltipRect.height/2}px`;
                            }

                            let left = rect.left;
                            if (left + tooltipRect.width > window.innerWidth - 20) {
                                left = window.innerWidth - tooltipRect.width - 20;
                            }
                            tooltip.style.left = `${Math.max(20, left)}px`;
                        });

                        button.addEventListener('mouseleave', () => {
                            tooltip.style.opacity = '0';
                            tooltip.style.transform = 'translateY(4px)';
                        });

                        button.onmouseover = () => !item.selected && (button.style.cssText = getButtonStyles(item.selected, true));
                        button.onmouseout = () => !item.selected && (button.style.cssText = getButtonStyles(item.selected));

                        button.onclick = (e) => {
                            if (item.selected) return;
                            e.stopPropagation();
                            const textarea = document.querySelector('textarea.w-full');
                            if (textarea) {
                                const currentValue = textarea.value;
                                setTextareaValue(textarea, currentValue + (currentValue && currentValue.trim() ? '  ' : '') + item.prompt);
                                item.selected = true;
                                button.style.cssText = getButtonStyles(true);
                            }
                        };

                        buttonsContainer.appendChild(button);
                    });

                    groupDiv.appendChild(buttonsContainer);
                    container.appendChild(groupDiv);
                });
            } catch (error) {
                console.error('Error rendering buttons:', error);
            }
        };

        // 初始渲染
        renderButtons();

        // 插入容器
        targetElement.insertBefore(container, targetElement.firstChild);

        // 显示/隐藏容器的函数
        const showContainer = () => {
            container.style.display = 'block';
            setTimeout(() => {
                container.style.opacity = '1';
                container.style.transform = 'translateY(0)';
            }, 10);
        };

        const hideContainer = () => {
            promptList.forEach(item => item.selected = false);
            container.innerHTML = '';
            renderButtons();
            container.style.opacity = '0';
            container.style.transform = 'translateY(-10px)';
            tooltip.style.opacity = '0';
            tooltip.style.transform = 'translateY(4px)';
            setTimeout(() => {
                container.style.display = 'none';
            }, 200);
        };

        // 事件监听
        textarea.addEventListener('focus', showContainer);
        document.addEventListener('click', (e) => {
            if (!container.contains(e.target) && e.target !== textarea) {
                hideContainer();
            }
        });
    }

    // 使用MutationObserver监听DOM变化
    const observer = new MutationObserver((mutations, obs) => {
        const targetElement = document.querySelector('.z-prompt');
        if (targetElement) {
            init();
            obs.disconnect();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();