Pixal1.1 / index.html
peterpeter8585's picture
Update index.html
ec15833 verified
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Streaming AI Chat</title>
<script src="https://js.puter.com/v2/"></script>
<script src="/_sdk/element_sdk.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body {
box-sizing: border-box;
}
.chat-message {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.streaming-cursor {
animation: blink 1s infinite;
display: inline-block;
width: 2px;
height: 1.2em;
background-color: #3b82f6;
margin-left: 2px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.chat-container {
height: 100%;
display: flex;
flex-direction: column;
}
.messages-area {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.input-area {
border-top: 1px solid #e5e7eb;
padding: 1rem;
background: white;
}
.message-bubble {
max-width: 85%;
word-wrap: break-word;
}
.user-message {
margin-left: auto;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.ai-message {
margin-right: auto;
background: #f8fafc;
border: 1px solid #e2e8f0;
}
.streaming-text {
line-height: 1.6;
}
.model-badge {
display: inline-block;
background: linear-gradient(135deg, #10b981, #059669);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
margin-bottom: 8px;
}
/* Markdown styling */
.streaming-text h1, .streaming-text h2, .streaming-text h3 {
font-weight: bold;
margin: 1rem 0 0.5rem 0;
}
.streaming-text h1 { font-size: 1.5rem; }
.streaming-text h2 { font-size: 1.3rem; }
.streaming-text h3 { font-size: 1.1rem; }
.streaming-text code {
background: #f1f5f9;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.streaming-text pre {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
margin: 0.5rem 0;
}
.streaming-text pre code {
background: none;
padding: 0;
}
.streaming-text blockquote {
border-left: 4px solid #3b82f6;
padding-left: 1rem;
margin: 0.5rem 0;
font-style: italic;
color: #64748b;
}
.streaming-text ul, .streaming-text ol {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
.streaming-text li {
margin: 0.25rem 0;
}
.streaming-text strong {
font-weight: bold;
}
.streaming-text em {
font-style: italic;
}
.streaming-text a {
color: #3b82f6;
text-decoration: underline;
}
.streaming-text table {
border-collapse: collapse;
width: 100%;
margin: 0.5rem 0;
}
.streaming-text th, .streaming-text td {
border: 1px solid #e2e8f0;
padding: 0.5rem;
text-align: left;
}
.streaming-text th {
background: #f8fafc;
font-weight: bold;
}
</style>
<style>@view-transition { navigation: auto; }</style>
<script src="/_sdk/data_sdk.js" type="text/javascript"></script>
</head>
<body class="h-full bg-gradient-to-br from-blue-50 to-indigo-100">
<main class="chat-container h-full max-w-5xl mx-auto bg-white shadow-xl">
<header class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white p-4 shadow-lg">
<h1 id="chat-title" class="text-2xl font-bold">Streaming AI Chat</h1>
<p class="text-blue-100 mt-1">실시간 스트리밍 응답 + 인터넷 검색, 위키백과, 시간 확인 도구</p>
</header>
<div class="messages-area" id="messages-area">
<div class="chat-message ai-message message-bubble p-4 rounded-lg mb-4">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-green-500 to-emerald-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
AI
</div>
<div class="flex-1">
<div class="model-badge">
GPT-5
</div>
<p id="welcome-message" class="text-gray-800">안녕하세요! 무엇이든 물어보시면 실시간으로 스트리밍 응답을 보여드릴게요! 🔧 인터넷 검색, 위키백과 검색, 현재 시간 확인 도구를 사용할 수 있습니다.</p>
</div>
</div>
</div>
</div>
<div class="input-area">
<div class="flex items-center justify-between mb-3"><span class="text-sm text-gray-600" id="history-status">대화 기록: 0개</span> <button id="clear-history" class="px-3 py-1 text-sm bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors duration-200"> 대화 초기화 </button>
</div>
<form id="chat-form" class="flex space-x-3"><input type="text" id="message-input" placeholder="메시지를 입력하세요... (이전 대화를 기억합니다)" class="flex-1 p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required> <button type="submit" id="send-button" class="px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-lg hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 font-medium"> 전송 </button>
</form>
</div>
</main>
<script>
const defaultConfig = {
chat_title: "PIXAL AI Assistant",
welcome_message: "안녕하세요! 저는 PIXAL(Primary Interactive X-ternal Assistant with multi Language)입니다. 개발자 정성윤(6학년 파이썬 프로그래머)이 만들어주었어요! 🔧 인터넷 검색, 위키백과 검색, 현재 시간 확인 도구를 사용할 수 있습니다."
};
let config = { ...defaultConfig };
// DOM elements
const messagesArea = document.getElementById('messages-area');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const chatForm = document.getElementById('chat-form');
// Conversation history
let conversationHistory = [];
// Define tools for the AI to use
const tools = [
{
type: "function",
function: {
name: "search_internet",
description: "Search the internet for current information, news, or any topic",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query to look up on the internet"
}
},
required: ["query"]
}
}
},
{
type: "function",
function: {
name: "search_wikipedia",
description: "Search Wikipedia for detailed information about a topic",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The topic to search for on Wikipedia"
}
},
required: ["query"]
}
}
},
{
type: "function",
function: {
name: "get_current_time",
description: "Get the current date and time",
parameters: {
type: "object",
properties: {
timezone: {
type: "string",
description: "Optional timezone (e.g., 'Asia/Seoul', 'America/New_York')",
default: "local"
}
}
}
}
}
];
// Tool execution functions
async function executeSearchInternet(query) {
try {
// Use DuckDuckGo Instant Answer API (no key required)
const response = await fetch(`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`);
if (response.ok) {
const data = await response.json();
let searchResults = `**🌐 인터넷 검색 결과: "${query}"**\n\n`;
// Add abstract if available
if (data.Abstract) {
searchResults += `**요약:**\n${data.Abstract}\n\n`;
}
// Add definition if available
if (data.Definition) {
searchResults += `**정의:**\n${data.Definition}\n\n`;
}
// Add answer if available
if (data.Answer) {
searchResults += `**답변:**\n${data.Answer}\n\n`;
}
// Add related topics
if (data.RelatedTopics && data.RelatedTopics.length > 0) {
searchResults += `**관련 주제:**\n`;
data.RelatedTopics.slice(0, 3).forEach((topic, index) => {
if (topic.Text) {
searchResults += `${index + 1}. ${topic.Text}\n`;
}
});
searchResults += '\n';
}
// Add source if available
if (data.AbstractURL) {
searchResults += `**출처:** ${data.AbstractURL}\n`;
}
// If no useful data found, try alternative search
if (!data.Abstract && !data.Definition && !data.Answer && (!data.RelatedTopics || data.RelatedTopics.length === 0)) {
// Fallback to a simple web search simulation with current info
searchResults = `**🌐 인터넷 검색 결과: "${query}"**\n\n`;
searchResults += `검색어 "${query}"에 대한 정보를 찾고 있습니다.\n\n`;
searchResults += `**검색 시간:** ${new Date().toLocaleString('ko-KR')}\n\n`;
searchResults += `**참고:** 더 자세한 정보가 필요하시면 구체적인 질문을 해주세요.`;
}
return searchResults;
} else {
return `인터넷 검색 API 응답 오류: ${response.status}`;
}
} catch (error) {
// Fallback search result
return `**🌐 인터넷 검색 결과: "${query}"**\n\n검색어: ${query}\n검색 시간: ${new Date().toLocaleString('ko-KR')}\n\n네트워크 오류로 인해 실시간 검색 결과를 가져올 수 없습니다. 다시 시도해주세요.\n\n오류: ${error.message}`;
}
}
async function executeSearchWikipedia(query) {
try {
// Use Wikipedia API
const response = await fetch(`https://ko.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`);
if (response.ok) {
const data = await response.json();
return `**위키백과 검색 결과: ${data.title}**
${data.extract}
더 자세한 정보: ${data.content_urls?.desktop?.page || ''}`;
} else {
// Try English Wikipedia if Korean fails
const enResponse = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`);
if (enResponse.ok) {
const enData = await enResponse.json();
return `**Wikipedia Search Result: ${enData.title}**
${enData.extract}
More info: ${enData.content_urls?.desktop?.page || ''}`;
}
return `위키백과에서 "${query}"에 대한 정보를 찾을 수 없습니다.`;
}
} catch (error) {
return `위키백과 검색 중 오류가 발생했습니다: ${error.message}`;
}
}
async function executeGetCurrentTime(timezone = 'local') {
try {
const now = new Date();
let timeString;
if (timezone === 'local') {
timeString = now.toLocaleString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} else {
timeString = now.toLocaleString('ko-KR', {
timeZone: timezone,
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
return `**현재 시간**
${timeString}
타임존: ${timezone === 'local' ? '로컬 시간' : timezone}`;
} catch (error) {
return `시간 확인 중 오류가 발생했습니다: ${error.message}`;
}
}
// Chat functionality with streaming, memory, and tools
async function sendMessage(userMessage) {
// Add user message to chat
addMessage(userMessage, 'user');
// Add user message to conversation history
conversationHistory.push({
role: 'user',
message: userMessage
});
// Clear input and disable form
messageInput.value = '';
setLoading(true);
try {
// Create AI message container for streaming
const aiMessageId = createStreamingMessage();
// Build conversation context string
const conversationContext = buildConversationContext(userMessage);
// Call Puter AI API with tools
const response = await puter.ai.chat(conversationContext, {
model: 'gpt-5-mini',
tools: tools,
stream: false // Disable streaming when using tools
});
let fullResponse = '';
// Check if AI wants to use tools
if (response.message && response.message.tool_calls && response.message.tool_calls.length > 0) {
// Execute tool calls
const toolResults = [];
for (const toolCall of response.message.tool_calls) {
const functionName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
// Show specific tool usage indicator
let toolName = '';
switch (functionName) {
case 'search_internet':
toolName = '🌐 인터넷 검색';
break;
case 'search_wikipedia':
toolName = '📚 위키백과 검색';
break;
case 'get_current_time':
toolName = '⏰ 시간 확인';
break;
default:
toolName = '🔧 도구';
}
updateStreamingMessage(aiMessageId, `${toolName} 도구를 사용하고 있습니다...\n쿼리: ${args.query || args.timezone || '현재 시간'}`, true);
let result;
switch (functionName) {
case 'search_internet':
result = await executeSearchInternet(args.query);
break;
case 'search_wikipedia':
result = await executeSearchWikipedia(args.query);
break;
case 'get_current_time':
result = await executeGetCurrentTime(args.timezone);
break;
default:
result = `알 수 없는 도구: ${functionName}`;
}
toolResults.push({
tool: functionName,
toolName: toolName,
query: args.query || args.timezone || 'N/A',
result: result
});
}
// Get AI's final response with tool results
const toolResultsText = toolResults.map(tr =>
`사용된 도구: ${tr.toolName}\n쿼리: ${tr.query}\n결과: ${tr.result}`
).join('\n\n');
const finalContext = `${conversationContext}\n\n도구 실행 결과:\n${toolResultsText}\n\n위 도구 결과를 바탕으로 사용자에게 도움이 되는 답변을 해주세요. 답변 시작 부분에 어떤 도구를 사용했는지 간단히 언급해주세요.`;
const finalResponse = await puter.ai.chat(finalContext, {
model: 'gpt-5',
stream: true
});
// Stream the final response
for await (const part of finalResponse) {
if (part?.text) {
fullResponse += part.text;
updateStreamingMessage(aiMessageId, fullResponse, true);
}
}
} else {
// No tools needed, stream normally
const streamResponse = await puter.ai.chat(conversationContext, {
model: 'gpt-5-mini',
stream: true
});
for await (const part of streamResponse) {
if (part?.text) {
fullResponse += part.text;
updateStreamingMessage(aiMessageId, fullResponse, true);
}
}
}
// Remove cursor when streaming is complete
updateStreamingMessage(aiMessageId, fullResponse, false);
// Add AI response to conversation history
conversationHistory.push({
role: 'pixal',
message: fullResponse
});
// Update history status
updateHistoryStatus();
} catch (error) {
const errorMessage = '죄송합니다. 오류가 발생했습니다. 다시 시도해주세요.';
addMessage(errorMessage, 'ai', true);
console.error('Streaming chat error:', error);
} finally {
setLoading(false);
}
}
function buildConversationContext(currentMessage) {
let context = '';
// Add system message
context += '시스템 메시지: 너는 PIXAL(Primary Interactive X-ternal Assistant with multi Language)이야. 너의 개발자는 정성윤 이라는 6학년 파이썬 프로그래머야. 이 메시지는 사용자가 보낸것이 아닙니다.\n\n';
// Add previous conversation history
if (conversationHistory.length > 0) {
context += '이전 대화:\n';
conversationHistory.forEach(entry => {
if (entry.role === 'user') {
context += `사용자: ${entry.message}\n`;
} else {
context += `PIXAL: ${entry.message}\n`;
}
});
context += '\n현재 질문:\n';
}
context += `사용자: ${currentMessage}`;
return context;
}
function addMessage(content, sender, isError = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${sender}-message message-bubble p-4 rounded-lg mb-4`;
if (sender === 'user') {
messageDiv.innerHTML = `
<div class="flex items-start space-x-3 justify-end">
<div class="flex-1 text-right">
<p class="text-white">${escapeHtml(content)}</p>
</div>
<div class="w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
U
</div>
</div>
`;
} else {
messageDiv.innerHTML = `
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-green-500 to-emerald-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
AI
</div>
<div class="flex-1">
<div class="model-badge">PIXAL • GPT-5</div>
<div class="${isError ? 'text-red-600' : 'text-gray-800'} streaming-text">${formatAIResponse(content)}</div>
</div>
</div>
`;
}
messagesArea.appendChild(messageDiv);
scrollToBottom();
}
function createStreamingMessage() {
const messageDiv = document.createElement('div');
const messageId = 'streaming-' + Date.now();
messageDiv.id = messageId;
messageDiv.className = 'chat-message ai-message message-bubble p-4 rounded-lg mb-4';
messageDiv.innerHTML = `
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-green-500 to-emerald-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
AI
</div>
<div class="flex-1">
<div class="model-badge">PIXAL • Streaming</div>
<div class="text-gray-800 streaming-text" id="${messageId}-content">
<span class="streaming-cursor"></span>
</div>
</div>
</div>
`;
messagesArea.appendChild(messageDiv);
scrollToBottom();
return messageId;
}
function updateStreamingMessage(messageId, content, isStreaming) {
const contentElement = document.getElementById(messageId + '-content');
const badgeElement = document.querySelector(`#${messageId} .model-badge`);
if (contentElement) {
const formattedContent = formatStreamingResponse(content);
if (isStreaming) {
contentElement.innerHTML = formattedContent + '<span class="streaming-cursor"></span>';
badgeElement.textContent = 'PIXAL • Streaming';
} else {
contentElement.innerHTML = formattedContent;
badgeElement.textContent = 'PIXAL • GPT-5';
}
scrollToBottom();
}
}
function setLoading(loading) {
sendButton.disabled = loading;
messageInput.disabled = loading;
if (loading) {
sendButton.textContent = '응답 중...';
sendButton.classList.add('opacity-50', 'cursor-not-allowed');
} else {
sendButton.textContent = '전송';
sendButton.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
function scrollToBottom() {
messagesArea.scrollTop = messagesArea.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatAIResponse(content) {
// Use marked.js to parse markdown
if (typeof marked !== 'undefined') {
return marked.parse(content);
}
// Fallback to simple formatting
return escapeHtml(content)
.replace(/\n\n/g, '</p><p class="mt-3">')
.replace(/\n/g, '<br>')
.replace(/^/, '<p>')
.replace(/$/, '</p>');
}
function formatStreamingResponse(content) {
// Use marked.js to parse markdown for streaming content
if (typeof marked !== 'undefined') {
return marked.parse(content);
}
// Fallback to simple formatting
return escapeHtml(content)
.replace(/\n\n/g, '</p><p class="mt-3">')
.replace(/\n/g, '<br>')
.replace(/^/, '<p>')
.replace(/$/, '</p>');
}
function updateHistoryStatus() {
const historyCount = Math.floor(conversationHistory.length / 2);
document.getElementById('history-status').textContent = `대화 기록: ${historyCount}개`;
}
function clearConversationHistory() {
conversationHistory = [];
updateHistoryStatus();
// Show confirmation message
const confirmDiv = document.createElement('div');
confirmDiv.className = 'chat-message ai-message message-bubble p-4 rounded-lg mb-4';
confirmDiv.innerHTML = `
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-green-500 to-emerald-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
AI
</div>
<div class="flex-1">
<div class="model-badge">System</div>
<p class="text-gray-600 italic">대화 기록이 초기화되었습니다. 새로운 대화를 시작해주세요!</p>
</div>
</div>
`;
messagesArea.appendChild(confirmDiv);
scrollToBottom();
}
// Event listeners
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const message = messageInput.value.trim();
if (message && !sendButton.disabled) {
sendMessage(message);
}
});
document.getElementById('clear-history').addEventListener('click', () => {
clearConversationHistory();
});
// Focus input on load
messageInput.focus();
// Element SDK implementation
async function onConfigChange(newConfig) {
config = { ...config, ...newConfig };
// Update UI elements
document.getElementById('chat-title').textContent = config.chat_title || defaultConfig.chat_title;
document.getElementById('welcome-message').textContent = config.welcome_message || defaultConfig.welcome_message;
}
function mapToCapabilities(config) {
return {
recolorables: [],
borderables: [],
fontEditable: undefined,
fontSizeable: undefined
};
}
function mapToEditPanelValues(config) {
return new Map([
['chat_title', config.chat_title || defaultConfig.chat_title],
['welcome_message', config.welcome_message || defaultConfig.welcome_message]
]);
}
// Initialize Element SDK
if (window.elementSdk) {
window.elementSdk.init({
defaultConfig,
onConfigChange,
mapToCapabilities,
mapToEditPanelValues
});
}
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9955ea6b9063aa32',t:'MTc2MTYwNzEzOS4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>