Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ dist-ssr
*.sln
*.sw?
.env

# Prevent accidental commit of test/junk files
abc.txt
demo.txt
1 change: 0 additions & 1 deletion abc.txt

This file was deleted.

2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI-Assistant Client</title>
<title>AI Assistant Client</title>
<meta name="description" content="Beautiful AI assistant client for interacting with multiple language models">
</head>
<body>
Expand Down
194 changes: 110 additions & 84 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,91 +7,117 @@ import { useChat } from './hooks/useChat';
import { availableModels } from './data/models';

function App() {
const [selectedModel, setSelectedModel] = React.useState('gpt-4-turbo');
const { messages, isLoading, sendMessage, clearChat } = useChat(selectedModel);

// Get the last user message (question)
const lastUserMessage = React.useMemo(() => {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === 'user') {
return messages[i];
}
}
return undefined;
}, [messages]);

return (
<div className="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900">
<div className="container mx-auto px-4 py-6 h-screen flex flex-col max-w-4xl">
{/* Header */}
<header className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-400 to-purple-500 rounded-xl flex items-center justify-center">
<Sparkles className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">AI Assistant</h1>
<p className="text-sm text-gray-300">Choose your model and start chatting</p>
</div>
</div>
const [selectedModel, setSelectedModel] = React.useState('gpt-4-turbo');
const { messages, isLoading, sendMessage, clearChat } = useChat(selectedModel);

// Get the last user message (question)
const lastUserMessage = React.useMemo(() => {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === 'user') {
return messages[i];
}
}
return undefined;
}, [messages]);

return (
<div className="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900">
<div className="container mx-auto px-4 py-6 h-screen flex flex-col max-w-4xl">
{/* Header */}
<header className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-400 to-purple-500 rounded-xl flex items-center justify-center">
<Sparkles className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">AI Assistant</h1>
<p className="text-sm text-gray-300">Choose your model and start chatting</p>
</div>
</div>

{messages.length > 0 && (
<button
onClick={clearChat}
className="flex items-center gap-2 px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-300 rounded-lg transition-colors duration-200"
aria-label="Clear chat"
>
<Trash2 className="w-4 h-4" />
Clear Chat
</button>
)}
</header>

{/* Model Selection */}
<div className="mb-6">
<ModelSelector
models={availableModels}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
/>
</div>

{/* Main Content with Sidebar */}
<div className="flex-1 flex bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl overflow-hidden">
{/* Left Sidebar for Last Question */}
<div className="w-1/4 bg-white/10 border-r border-white/10 p-4 flex flex-col items-start justify-start">
<h2 className="text-sm font-semibold text-gray-300 mb-2">Last Question</h2>
{lastUserMessage ? (
<div
className="text-gray-100 text-base break-words"
dangerouslySetInnerHTML={{ __html: lastUserMessage.content }}
/>
) : (
<div className="text-gray-400 text-sm">No question asked yet.</div>
)}
</div>

{/* Chat Area */}
<div className="flex-1 flex flex-col">
<ChatArea messages={messages} isLoading={isLoading} />
{/* Input Area */}
<div className="p-4 border-t border-white/10">
<ChatInput
onSendMessage={sendMessages}
disabled={isLoading || !selectedModel}
placeholder={selectedModel ? "Ask me anything..." : "Select a model to start chatting"}
/>
</div>
</div>
</div>

{/* Footer */}
<footer className="mt-4 text-center text-sm text-gray-400">
AI Assistant Client v1.0 - Built with React & TypeScript
</footer>
</div>
</div>
);
{messages.length > 0 && (
<button
onClick={clearChat}
className="flex items-center gap-2 px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-300 rounded-lg transition-colors duration-200"
aria-label="Clear chat"
>
<Trash2 className="w-4 h-4" />
Clear Chat
</button>
)}
</header>

{/* Model Selection */}
<div className="mb-6">
<ModelSelector
models={availableModels}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
/>
</div>

{/* Main Content with Sidebar */}
<div className="flex-1 flex bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl overflow-hidden">
{/* Left Sidebar for Last Question */}
<div className="w-1/4 bg-white/10 border-r border-white/10 p-4 flex flex-col items-start justify-start">
<h2 className="text-sm font-semibold text-gray-300 mb-2">Last Question</h2>
{lastUserMessage ? (
<div
className="text-gray-100 text-base break-words"
dangerouslySetInnerHTML={{ __html: lastUserMessage.content }}
/>
Comment on lines +64 to +68
Copy link

@coderabbitai coderabbitai bot Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical XSS vulnerability: Unsanitized user input rendered as HTML.

Using dangerouslySetInnerHTML with user-provided content (lastUserMessage.content) creates a cross-site scripting (XSS) vulnerability. Malicious users can inject arbitrary JavaScript by submitting messages like <script>alert('XSS')</script> or <img src=x onerror="steal_cookies()">.

Recommended fix: Render as plain text

Apply this diff to safely display user content:

-						<div
-							className="text-gray-100 text-base break-words"
-							dangerouslySetInnerHTML={{ __html: lastUserMessage.content }}
-						/>
+						<div className="text-gray-100 text-base break-words">
+							{lastUserMessage.content}
+						</div>

Alternative fix: Sanitize HTML with DOMPurify (only if HTML rendering is required)

If you need to support formatted content, install and use DOMPurify:

npm install dompurify
npm install --save-dev @types/dompurify

Then sanitize before rendering:

+import DOMPurify from 'dompurify';
+
 function App() {
 	// ... existing code ...
 	
 	return (
 		// ... existing JSX ...
 		<div
 			className="text-gray-100 text-base break-words"
-			dangerouslySetInnerHTML={{ __html: lastUserMessage.content }}
+			dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(lastUserMessage.content) }}
 		/>
 	);
 }
🧰 Tools
🪛 ast-grep (0.40.0)

[warning] 66-66: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)

[error] 67-67: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In src/App.tsx around lines 64-68, the code uses dangerouslySetInnerHTML with
lastUserMessage.content which creates a critical XSS risk; fix it by rendering
the message as plain text (remove dangerouslySetInnerHTML and render the content
as a child or use React's text escaping) and if HTML formatting is required,
sanitize lastUserMessage.content with DOMPurify before passing it to
dangerouslySetInnerHTML (install dompurify and @types/dompurify, import
DOMPurify, call DOMPurify.sanitize(content) and use the sanitized result).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai
Generate a new issue for this

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Create a GitHub issue for the XSS vulnerability

gh issue create \
  --repo manishcy/chatAI \
  --title "Critical XSS vulnerability in src/App.tsx using dangerouslySetInnerHTML" \
  --body "## Description

A critical XSS (Cross-Site Scripting) vulnerability has been identified in \`src/App.tsx\` where unsanitized user input is rendered using \`dangerouslySetInnerHTML\`.

## Location

- File: \`src/App.tsx\`
- Lines: 64-68 (approximately)
- Component: App component, Last Question sidebar section

## Issue

The code uses \`dangerouslySetInnerHTML\` with \`lastUserMessage.content\` which comes from user input. This creates a security vulnerability where malicious users can inject arbitrary JavaScript.

## References

- Pull Request: https://github.com/manishcy/chatAI/pull/8
- Review Comment: https://github.com/manishcy/chatAI/pull/8#discussion_r2579965345
- Requested by: @manishcy

## Priority

🔴 Critical - This is a security vulnerability that should be addressed as soon as possible." \
  --assignee manishcy

Length of output: 43


@manishcy I've successfully created a new issue for the XSS vulnerability!

Issue created: #9 - Critical XSS vulnerability in src/App.tsx using dangerouslySetInnerHTML

The issue includes:

  • Description of the security vulnerability
  • File location and affected lines
  • References to this PR and review comment
  • Priority marked as 🔴 Critical
  • Assigned to you

You can track and address this security issue in a separate PR.


🧬 Code Graph Analysis Results
  • File: src/data/models.ts (lines 3-39)
export const availableModels: AIModel[] = [
  {
    id: 'gpt-4-turbo',
    name: 'GPT-4 Turbo',
    description: 'Most capable model for complex reasoning and analysis',
    provider: 'OpenAI',
    capabilities: ['Text Generation', 'Code Analysis', 'Mathematical Reasoning', 'Creative Writing']
  },
  {
    id: 'gpt-3.5-turbo',
    name: 'GPT-3.5 Turbo',
    description: 'Fast and efficient for most conversational tasks',
    provider: 'OpenAI',
    capabilities: ['Text Generation', 'Basic Coding', 'Q&A', 'Summarization']
  },
  {
    id: 'claude-3-opus',
    name: 'Claude 3 Opus',
    description: 'Excellent for analysis, math, and creative tasks',
    provider: 'Anthropic',
    capabilities: ['Advanced Analysis', 'Creative Writing', 'Code Review', 'Research']
  },
  {
    id: 'claude-3-sonnet',
    name: 'Claude 3 Sonnet',
    description: 'Balanced performance for everyday tasks',
    provider: 'Anthropic',
    capabilities: ['Text Generation', 'Code Help', 'Analysis', 'Writing']
  },
  {
    id: 'gemini-pro',
    name: 'Gemini Pro',
    description: 'Google\'s advanced multimodal AI model',
    provider: 'Google',
    capabilities: ['Text Generation', 'Multimodal', 'Code Generation', 'Reasoning']
  }
];
  • File: src/hooks/useChat.ts (lines 5-65)
export const useChat = (selectedModel: string) => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  const sendMessage = useCallback(async (content: string) => {
    if (!selectedModel) {
      alert('Please select a model first');
      return;
    }

    const userMessage: Message = {
      id: Date.now().toString(),
      content,
      role: 'user',
      timestamp: new Date(),
    };

    setMessages(prev => [...prev, userMessage]);
    setIsLoading(true);

    try {
      const response = await AIService.generateResponse(
        [...messages, userMessage],
        selectedModel
      );

      const assistantMessage: Message = {
        id: (Date.now() + 1).toString(),
        content: response,
        role: 'assistant',
        timestamp: new Date(),
        model: selectedModel,
      };

      setMessages(prev => [...prev, assistantMessage]);
    } catch (error) {
      console.error('Error generating response:', error);
      const errorMessage: Message = {
        id: (Date.now() + 1).toString(),
        content: 'Sorry, I encountered an error while generating a response. Please try again.',
        role: 'assistant',
        timestamp: new Date(),
        model: selectedModel,
      };
      setMessages(prev => [...prev, errorMessage]);
    } finally {
      setIsLoading(false);
    }
  }, [messages, selectedModel]);

  const clearChat = useCallback(() => {
    setMessages([]);
  }, []);

  return {
    messages,
    isLoading,
    sendMessage,
    clearChat,
  };
};
  • File: src/components/ChatArea.tsx (lines 10-55)
export const ChatArea: React.FC<ChatAreaProps> = ({ messages, isLoading }) => {
  const messagesEndRef = React.useRef<HTMLDivElement>(null);

  React.useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  if (messages.length === 0) {
    return (
      <div className="flex-1 flex items-center justify-center">
        <div className="text-center text-gray-400 max-w-md">
          <div className="text-6xl mb-4">🤖</div>
          <h3 className="text-xl font-semibold mb-2">Welcome to AI Assistant</h3>
          <p className="text-sm">
            Select a model above and start a conversation. Ask me anything!
          </p>
        </div>
      </div>
    );
  }

  return (
    <div className="flex-1 overflow-y-auto px-4 py-6 space-y-6">
      {messages.map((message) => (
        <MessageBubble key={message.id} message={message} />
      ))}
      
      {isLoading && (
        <div className="flex gap-3">
          <div className="w-8 h-8 rounded-full bg-gray-600 flex items-center justify-center">
            <div className="w-2 h-2 bg-white rounded-full animate-pulse" />
          </div>
          <div className="bg-white/90 backdrop-blur-sm border border-gray-200 rounded-2xl rounded-tl-md px-4 py-3">
            <div className="flex gap-1">
              <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
              <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
              <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
            </div>
          </div>
        </div>
      )}
      
      <div ref={messagesEndRef} />
    </div>
  );
};
  • File: src/components/ChatInput.tsx (lines 10-78)
export const ChatInput: React.FC<ChatInputProps> = ({
  onSendMessage,
  disabled = false,
  placeholder = "Type your message..."
}) => {
  const [message, setMessage] = React.useState('');
  const textareaRef = React.useRef<HTMLTextAreaElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (message.trim() && !disabled) {
      onSendMessage(message.trim());
      setMessage('');
      if (textareaRef.current) {
        textareaRef.current.style.height = 'auto';
      }
    }
  };

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

  const adjustHeight = () => {
    const textarea = textareaRef.current;
    if (textarea) {
      textarea.style.height = 'auto';
      textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
    }
  };

  React.useEffect(() => {
    adjustHeight();
  }, [message]);

  return (
    <form onSubmit={handleSubmit} className="flex gap-3 items-end">
      <div className="flex-1 relative">
        <textarea
          ref={textareaRef}
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder={placeholder}
          disabled={disabled}
          className="w-full px-4 py-3 pr-12 bg-white/90 backdrop-blur-sm border border-gray-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 disabled:opacity-50"
          rows={1}
          style={{ minHeight: '52px', maxHeight: '120px' }}
        />
      </div>
      
      <button
        type="submit"
        disabled={!message.trim() || disabled}
        className="flex-shrink-0 w-12 h-12 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white rounded-xl flex items-center justify-center transition-colors duration-200 disabled:cursor-not-allowed"
        aria-label="Send message"
      >
        {disabled ? (
          <Loader2 className="w-5 h-5 animate-spin" />
        ) : (
          <Send className="w-5 h-5" />
        )}
      </button>
    </form>
  );
};
  • File: src/components/ModelSelector.tsx (lines 11-73)
export const ModelSelector: React.FC<ModelSelectorProps> = ({
  models,
  selectedModel,
  onModelChange,
}) => {
  const [isOpen, setIsOpen] = React.useState(false);
  const selectedModelData = models.find(m => m.id === selectedModel);

  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-3 w-full px-4 py-3 bg-white/10 backdrop-blur-sm border border-white/20 rounded-xl hover:bg-white/15 transition-all duration-200 group"
        aria-label="Select AI model"
      >
        <Bot className="w-5 h-5 text-blue-400" />
        <div className="flex-1 text-left">
          <div className="font-medium text-white">
            {selectedModelData?.name || 'Select Model'}
          </div>
          <div className="text-sm text-gray-300">
            {selectedModelData?.provider}
          </div>
        </div>
        <ChevronDown 
          className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${
            isOpen ? 'rotate-180' : ''
          }`} 
        />
      </button>

      {isOpen && (
        <div className="absolute top-full left-0 right-0 mt-2 bg-white/95 backdrop-blur-lg border border-gray-200 rounded-xl shadow-2xl overflow-hidden z-50">
          {models.map((model) => (
            <button
              key={model.id}
              onClick={() => {
                onModelChange(model.id);
                setIsOpen(false);
              }}
              className={`w-full px-4 py-3 text-left hover:bg-blue-50 transition-colors duration-150 border-b border-gray-100 last:border-b-0 ${
                selectedModel === model.id ? 'bg-blue-50 border-l-4 border-l-blue-500' : ''
              }`}
            >
              <div className="font-medium text-gray-900">{model.name}</div>
              <div className="text-sm text-gray-600 mb-1">{model.description}</div>
              <div className="flex gap-1 flex-wrap">
                {model.capabilities.slice(0, 3).map((cap) => (
                  <span
                    key={cap}
                    className="text-xs px-2 py-1 bg-gray-200 text-gray-700 rounded-full"
                  >
                    {cap}
                  </span>
                ))}
              </div>
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

) : (
<div className="text-gray-400 text-sm">No question asked yet.</div>
)}
</div>

{/* Chat Area */}
<div className="flex-1 flex flex-col">
<ChatArea messages={messages} isLoading={isLoading} />
{/* Input Area */}
<div className="p-4 border-t border-white/10">
<ChatInput
onSendMessage={sendMessage}
disabled={isLoading || !selectedModel}
placeholder={selectedModel ? "Ask me anything..." : "Select a model to start chatting"}
/>
</div>
</div>
</div>

{/* Footer */}
<footer className="mt-4 text-center text-sm text-gray-400">
AI Assistant Client v1.0 - Built with React & TypeScript
</footer>
</div>
</div>
);
}

export default App;


























Loading