InputBar.tsx 11 KB
import React, { useState, useEffect, KeyboardEvent } from 'react';
import { Loader2, ArrowUp, Sliders, Dices, X, RefreshCw } from 'lucide-react';
import { ImageGenerationParams } from '../types';

interface InputBarProps {
  onGenerate: (params: ImageGenerationParams) => void;
  isGenerating: boolean;
  incomingParams?: ImageGenerationParams | null; // For "Generate Similar"
}

const ASPECT_RATIOS = [
  { label: '1:1', w: 1024, h: 1024 },
  { label: '16:9', w: 1024, h: 576 },
  { label: '9:16', w: 576, h: 1024 },
  { label: '4:3', w: 1024, h: 768 },
  { label: '3:4', w: 768, h: 1024 },
  { label: 'Custom', w: 0, h: 0 },
];

const InputBar: React.FC<InputBarProps> = ({ onGenerate, isGenerating, incomingParams }) => {
  const [prompt, setPrompt] = useState('');
  const [showSettings, setShowSettings] = useState(false);

  // Parameters State
  const [width, setWidth] = useState(1024);
  const [height, setHeight] = useState(1024);
  const [steps, setSteps] = useState(8);
  const [guidance, setGuidance] = useState(0);
  const [seed, setSeed] = useState(12345);
  const [activeRatio, setActiveRatio] = useState('1:1');

  // Handle "Generate Similar" incoming data
  useEffect(() => {
    if (incomingParams) {
      setPrompt(incomingParams.prompt);
      setWidth(incomingParams.width);
      setHeight(incomingParams.height);
      setSteps(incomingParams.num_inference_steps);
      setGuidance(incomingParams.guidance_scale);
      setSeed(incomingParams.seed);
      
      // Match active ratio label
      const matched = ASPECT_RATIOS.find(r => r.w === incomingParams.width && r.h === incomingParams.height);
      setActiveRatio(matched ? matched.label : 'Custom');
      
      // Open settings so user can see what's loaded
      setShowSettings(true);
    }
  }, [incomingParams]);

  useEffect(() => {
    if (!incomingParams) {
      setSeed(Math.floor(Math.random() * 1000000));
    }
  }, []);

  const handleGenerate = () => {
    if (!prompt.trim() || isGenerating) return;
    
    const params: ImageGenerationParams = {
      prompt,
      width,
      height,
      num_inference_steps: steps,
      guidance_scale: guidance,
      seed,
    };

    onGenerate(params);
    
    // Note: Prompt is NO LONGER cleared as per user request
    // Regenerate seed for next time ONLY IF not explicit
    // setSeed(Math.floor(Math.random() * 1000000)); 
  };

  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleGenerate();
    }
  };

  const handleRatioSelect = (ratio: typeof ASPECT_RATIOS[0]) => {
    setActiveRatio(ratio.label);
    if (ratio.label !== 'Custom') {
      setWidth(ratio.w);
      setHeight(ratio.h);
    }
  };

  const randomizeSeed = () => {
    setSeed(Math.floor(Math.random() * 10000000));
  };

  return (
    <div className="fixed bottom-0 left-0 right-0 p-4 md:p-6 z-50 flex flex-col items-center pointer-events-none">
      
      {/* Advanced Settings Panel */}
      {showSettings && (
        <div className="pointer-events-auto w-full max-w-2xl bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl border border-gray-200 dark:border-gray-700 rounded-2xl shadow-2xl mb-4 p-5 animate-fade-in flex flex-col gap-5">
          <div className="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-3">
            <h3 className="font-bold text-gray-800 dark:text-white flex items-center gap-2">
              <Sliders size={18} className="text-purple-500" />
              生成参数设置 {incomingParams && <span className="text-xs bg-purple-100 text-purple-600 px-2 py-0.5 rounded-full ml-2">已加载同款参数</span>}
            </h3>
            <button onClick={() => setShowSettings(false)} className="text-gray-500 hover:text-gray-800 dark:hover:text-white p-1">
              <X size={18} />
            </button>
          </div>

          <div className="space-y-2">
            <label className="text-xs font-semibold text-gray-500 uppercase">分辨率 (宽高比)</label>
            <div className="flex flex-wrap gap-2">
              {ASPECT_RATIOS.map((r) => (
                <button
                  key={r.label}
                  onClick={() => handleRatioSelect(r)}
                  className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors border ${
                    activeRatio === r.label
                      ? 'bg-black dark:bg-white text-white dark:text-black border-transparent shadow-sm'
                      : 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800'
                  }`}
                >
                  {r.label}
                </button>
              ))}
            </div>
            {activeRatio === 'Custom' && (
              <div className="flex gap-4 mt-2 animate-fade-in">
                 <div className="flex items-center gap-2">
                   <span className="text-xs text-gray-400">W:</span>
                   <input 
                     type="number" 
                     min="64" max="2048"
                     value={width}
                     onChange={(e) => setWidth(Number(e.target.value))}
                     className="w-20 p-1 bg-gray-50 dark:bg-gray-800 border dark:border-gray-700 rounded text-center text-sm"
                   />
                 </div>
                 <div className="flex items-center gap-2">
                   <span className="text-xs text-gray-400">H:</span>
                   <input 
                     type="number" 
                     min="64" max="2048"
                     value={height}
                     onChange={(e) => setHeight(Number(e.target.value))}
                     className="w-20 p-1 bg-gray-50 dark:bg-gray-800 border dark:border-gray-700 rounded text-center text-sm"
                   />
                 </div>
              </div>
            )}
          </div>

          <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div className="space-y-2">
              <div className="flex justify-between">
                <label className="text-xs font-semibold text-gray-500 uppercase">生成步数 (Steps)</label>
                <span className="text-xs font-mono text-gray-800 dark:text-gray-200">{steps}</span>
              </div>
              <input 
                type="range" 
                min="6" 
                max="12" 
                step="1"
                value={steps}
                onChange={(e) => setSteps(Number(e.target.value))}
                className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-black dark:accent-white"
              />
              <div className="flex justify-between text-[10px] text-gray-400">
                <span>6</span>
                <span>12</span>
              </div>
            </div>

            <div className="space-y-2">
              <label className="text-xs font-semibold text-gray-500 uppercase block">引导系数 (Guidance)</label>
              <div className="flex items-center gap-2">
                <input 
                  type="number" 
                  min="0" 
                  max="10" 
                  step="0.1"
                  value={guidance}
                  onChange={(e) => setGuidance(Number(e.target.value))}
                  className="w-full p-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm"
                />
              </div>
            </div>
          </div>

          <div className="space-y-2">
             <label className="text-xs font-semibold text-gray-500 uppercase">随机种子 (Seed)</label>
             <div className="flex gap-2">
               <input 
                  type="number"
                  value={seed}
                  onChange={(e) => setSeed(Number(e.target.value))}
                  className="flex-1 p-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg font-mono text-sm"
               />
               <button 
                 onClick={randomizeSeed}
                 className="p-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors text-gray-600 dark:text-gray-300"
                 title="随机生成"
               >
                 <Dices size={20} />
               </button>
             </div>
          </div>
        </div>
      )}

      {/* Main Input Capsule */}
      <div className="pointer-events-auto w-full max-w-2xl transition-all duration-300 ease-in-out transform hover:scale-[1.01]">
        <div className="relative group">
          <div className="absolute inset-0 bg-white/80 dark:bg-black/80 backdrop-blur-2xl rounded-full shadow-2xl border border-white/20 dark:border-white/10" />
          
          <div className="relative flex items-center p-2 pr-2">
            <button
              onClick={() => setShowSettings(!showSettings)}
              className={`flex-shrink-0 w-10 h-10 md:w-12 md:h-12 flex items-center justify-center rounded-full ml-1 transition-all ${
                showSettings 
                  ? 'bg-purple-600 text-white shadow-lg rotate-90' 
                  : 'bg-gray-100 dark:bg-gray-800 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700'
              }`}
            >
               <Sliders size={20} />
            </button>

            <input
              type="text"
              value={prompt}
              onChange={(e) => setPrompt(e.target.value)}
              onKeyDown={handleKeyDown}
              disabled={isGenerating}
              placeholder="描述您的创意内容..."
              className="flex-grow bg-transparent border-none outline-none px-4 py-3 text-gray-800 dark:text-white placeholder-gray-400 text-base md:text-lg"
            />

            <div className="flex items-center gap-1">
              {/* Optional: Quick Regenerate button if prompt exists */}
              {prompt.trim() && !isGenerating && (
                 <button
                    onClick={handleGenerate}
                    className="p-3 text-gray-400 hover:text-purple-500 transition-colors"
                    title="重新生成"
                 >
                   <RefreshCw size={18} />
                 </button>
              )}
              
              <button
                onClick={handleGenerate}
                disabled={!prompt.trim() || isGenerating}
                className={`
                  flex items-center justify-center w-10 h-10 md:w-12 md:h-12 rounded-full transition-all duration-200
                  ${prompt.trim() && !isGenerating 
                    ? 'bg-black dark:bg-white text-white dark:text-black hover:scale-105 active:scale-95 shadow-md' 
                    : 'bg-gray-200 dark:bg-gray-700 text-gray-400 cursor-not-allowed'}
                `}
              >
                {isGenerating ? (
                  <Loader2 className="animate-spin" size={20} />
                ) : (
                  <ArrowUp size={20} strokeWidth={3} />
                )}
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default InputBar;