WORKSPACE / ЧАТ
ВАШ КЛЮЧ — ВАШ КОНТРОЛЬ

Чат с AI

Выберите бесплатную модель или подключите собственный AI API.

ORBIT / ГОТОВПривет! Это ORBIT — открытая AI-студия. Выберите одну из бесплатных моделей или подключите собственный API и напишите сообщение.
Прикреплённое изображение
Отправится в vision-модель вместе с сообщением
`; function securePreview(code){const csp=``;return /]*>/i.test(code)?code.replace(/]*)>/i,''+csp):''+csp+''+code+''} function runBuilder(){let code=$('#builderCode').value.trim();if(!code)return;$('#builderFrame').srcdoc=securePreview(code);$('#builderStatus').textContent='Предпросмотр обновлён. Код выполняется в изолированной песочнице.'} function extractHtml(text){let fenced=text.match(/```(?:html)?\s*([\s\S]*?)```/i);if(fenced)return fenced[1].trim();let at=text.search(/=0?text.slice(at).trim():text.trim()} async function streamCompletion(prompt,onChunk=()=>{}){let endpoint=$('#apiEndpoint').value.trim(),apiKey=$('#apiKey').value.trim(),model=$('#apiModel').value.trim();if(!endpoint||!apiKey||!model)throw new Error('Заполните endpoint, Model ID и API key.');let payload={model,messages:[{role:'system',content:'Верни только полный автономный HTML без markdown.'},{role:'user',content:prompt}],temperature:.45},res=await fetch('/api/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({endpoint,apiKey,payload,provider:$('#apiProvider').value})});if(!res.ok){let t=await res.text();try{let j=JSON.parse(t);throw new Error(j?.error?.message||t)}catch(e){throw e}}let reader=res.body.getReader(),dec=new TextDecoder(),buf='',out='';while(true){let {value,done}=await reader.read();if(done)break;buf+=dec.decode(value,{stream:true});let lines=buf.split(/\r?\n/);buf=lines.pop()||'';for(let line of lines){if(!line.startsWith('data:'))continue;let d=line.slice(5).trim();if(!d||d==='[DONE]')continue;try{let j=JSON.parse(d),part=j?.choices?.[0]?.delta?.content??j?.choices?.[0]?.text??j?.delta?.text??j?.candidates?.[0]?.content?.parts?.map(x=>x.text||'').join('')??'';if(part){out+=part;onChunk(out)}}catch{}}}if(!out&&buf){try{let j=JSON.parse(buf);out=j?.choices?.[0]?.message?.content||''}catch{out=buf}}if(!out)throw new Error('Провайдер завершил поток без кода.');return out} async function generateBuilderCode(request,onChunk=()=>{}){const instruction=`Создай законченную интерактивную игру или веб-приложение. Верни ТОЛЬКО компактный полный HTML без markdown. CSS и JavaScript внутри HTML. Без внешних библиотек и сетевых запросов. Сделай адаптивный интерфейс, правила и перезапуск. Запрос: ${request}`;if(isRemote())return extractHtml(await streamCompletion(instruction,x=>onChunk(extractHtml(x))));let g=await getGenerator(),o=await g([{role:'system',content:'Ты создаёшь автономные одностраничные HTML-приложения. Возвращай только полный HTML.'},{role:'user',content:instruction}],{max_new_tokens:4096,do_sample:true,temperature:.55,top_p:.9});let a=o[0].generated_text;return extractHtml(Array.isArray(a)?a.at(-1).content:a)} function createArtifactCard(){let card=document.createElement('div');card.className='message ai artifact-message';card.innerHTML='
ORBIT APP · LIVE PREVIEW
AI создаёт приложение…
Проектирую интерфейс и механику…
';$('#messages').append(card);$('#messages').scrollTop=999999;return card} function wireArtifact(card){let code=card.querySelector('.artifact-code'),status=card.querySelector('.artifact-status'),frame,wrap;const run=()=>{if(!frame){card.querySelector('.artifact-loading')?.remove();wrap=document.createElement('div');wrap.className='artifact-frame-wrap';frame=document.createElement('iframe');frame.className='artifact-frame';frame.sandbox='allow-scripts';frame.title='Live preview';wrap.append(frame);code.before(wrap)}frame.srcdoc=securePreview(code.value);status.textContent='Приложение запущено в безопасной песочнице.'};card.querySelector('[data-a="code"]').onclick=()=>code.classList.toggle('open');card.querySelector('[data-a="run"]').onclick=run;card.querySelector('[data-a="copy"]').onclick=async()=>{await navigator.clipboard.writeText(code.value);status.textContent='Код скопирован.'};card.querySelector('[data-a="download"]').onclick=()=>{let a=document.createElement('a'),u=URL.createObjectURL(new Blob([code.value],{type:'text/html'}));a.href=u;a.download='orbit-app.html';a.click();setTimeout(()=>URL.revokeObjectURL(u),1000)};card.querySelector('[data-a="full"]').onclick=()=>wrap?.requestFullscreen?.();card.querySelector('.artifact-refine button').onclick=async()=>{let change=card.querySelector('.artifact-refine input').value.trim();if(!change)return;status.textContent='AI вносит изменения…';try{code.value=await generateBuilderCode('Измени приложение: '+change+'\n\nТекущий HTML:\n'+code.value,x=>{code.value=x;status.textContent='Получаю изменения… '+x.length+' символов'});run();status.textContent='Изменения применены.'}catch(e){status.textContent='Ошибка: '+e.message}};return {code,status,run}} async function buildFromPrompt(raw){if(state.busy||!raw.trim())return;state.busy=true;$('#send').disabled=true;let card=createArtifactCard(),a=wireArtifact(card);try{let code=await generateBuilderCode(raw,x=>{a.code.value=x;a.status.textContent='Генерирую код… '+x.length+' символов';$('#messages').scrollTop=999999});if(!/x.classList.toggle('active',x.dataset.view===key));for(let x of ['chatView','writeView','visionView','radarView','historyView','guidesView','ideView'])$('#'+x).classList.add('hidden');let v=views[key];$('#'+v[0]).classList.remove('hidden');$('#title').textContent=v[1];$('#subtitle').textContent=v[2];$('#crumb').textContent=v[1].toUpperCase();$('#mobileView').value=key}document.querySelectorAll('.nav button').forEach(b=>b.onclick=()=>openView(b.dataset.view));$('#mobileView').onchange=e=>openView(e.target.value);$('#newChat').onclick=newConversation;$('#newChatMobile').onclick=newConversation;$('#refreshRadar').onclick=()=>refreshRadar().catch(e=>{$('#radarSummary').textContent='Ошибка проверки: '+e.message});document.querySelectorAll('.copy-guide').forEach(b=>b.onclick=async()=>{await navigator.clipboard.writeText(b.dataset.copy);let old=b.textContent;b.textContent='Скопировано ✓';setTimeout(()=>b.textContent=old,1200)}); $('#runText').onclick=async()=>{let text=$('#sourceText').value;if(!text)return;let r=$('#textResult');r.textContent='Модель думает…';try{let g=await getGenerator(),o=await g([{role:'system',content:'Ты редактор. Выполни инструкцию и верни только результат.'},{role:'user',content:$('#task').value+'\n\n'+text}],{max_new_tokens:420,do_sample:false});let a=o[0].generated_text;r.textContent=Array.isArray(a)?a.at(-1).content:a}catch(e){r.textContent='Ошибка: '+e.message}}; const drop=$('#drop'),input=$('#imageInput');let imageFile;function loadImage(f){if(!f?.type.startsWith('image/'))return;imageFile=f;let url=URL.createObjectURL(f);$('#preview').src=url;$('#preview').style.display='block';$('#runVision').disabled=false;$('#visionResult').textContent='Изображение готово к анализу.'}drop.onclick=()=>input.click();input.onchange=e=>loadImage(e.target.files[0]);['dragenter','dragover'].forEach(t=>drop.addEventListener(t,e=>{e.preventDefault();drop.classList.add('drag')}));['dragleave','drop'].forEach(t=>drop.addEventListener(t,e=>{e.preventDefault();drop.classList.remove('drag')}));drop.addEventListener('drop',e=>loadImage(e.dataTransfer.files[0]));$('#runVision').onclick=async()=>{let r=$('#visionResult');r.textContent='Анализирую…';try{let v=await getVision(),out=await v($('#preview').src,{topk:5});r.textContent=out.map((x,i)=>(i+1)+'. '+x.label+' — '+Math.round(x.score*100)+'%').join('\n')}catch(e){r.textContent='Ошибка: '+e.message}}; $('#apiProvider').onchange=()=>{applyProvider();apiMessage('Заполните ключ и сохраните настройки.')};$('#saveApi').onclick=saveApi;$('#testApi').onclick=async()=>{if(!saveApi())return;let b=$('#testApi');b.disabled=true;b.textContent='Проверяю…';apiMessage('Отправляю тестовый запрос…');try{await remoteCompletion('Ответь только словом OK',[],true);apiMessage('Подключение работает ✓ Можно пользоваться чатом.','ok')}catch(e){apiMessage('Ошибка подключения: '+e.message,'err')}finally{b.disabled=false;b.textContent='Проверить'}};$('#toggleKey').onclick=()=>{let k=$('#apiKey');k.type=k.type==='password'?'text':'password'};$('#clearApi').onclick=()=>{sessionStorage.removeItem('orbitApiConfig');sessionStorage.removeItem('orbitApiKey');$('#apiKey').value='';$('#rememberKey').checked=false;applyProvider();apiMessage('Ключ и настройки удалены.')};restoreApi(); async function clearDownloadedModels(){if(!confirm('Удалить скачанные локальные модели? Их придётся загрузить заново.'))return;setStatus('Очищаю кэш…',20);state.generators.clear();state.classifier=null;state.vision=null;let removed=0;try{if('caches' in window)for(const name of await caches.keys()){if(!name.startsWith('orbit-pwa-')){await caches.delete(name);removed++}}if(indexedDB.databases)for(const db of await indexedDB.databases()){if(db.name&&/transform|hugging|onnx|model/i.test(db.name)){indexedDB.deleteDatabase(db.name);removed++}}setStatus('Кэш моделей очищен',100);alert('Готово. Удалено хранилищ: '+removed)}catch(e){setStatus('Ошибка очистки',0);alert('Не удалось полностью очистить кэш: '+e.message)}} $('#attachImage').onclick=()=>$('#imageInput').click();$('#imageInput').onchange=async e=>{try{await imageFromFile(e.target.files[0])}catch(err){alert(err.message)}};document.querySelectorAll('[data-img]').forEach(b=>b.onclick=()=>editAttached(b.dataset.img));$('#removeImage').onclick=clearAttached;$('#downloadImage').onclick=()=>{if(!state.pendingImage)return;let a=document.createElement('a');a.href=state.pendingImage;a.download='orbit-edited.jpg';a.click()};$('#exportChats').onclick=exportChats;$('#exportChatsMobile').onclick=exportChats;$('#clearModels').onclick=clearDownloadedModels;$('#clearModelsMobile').onclick=clearDownloadedModels;$('#clearCache').onclick=()=>{if(confirm('Удалить всю историю чатов на этом устройстве?')){localStorage.removeItem(CHAT_STORE);state.chats=[];newConversation()}};loadStored();if(state.chats[0])openConversation(state.chats[0].id);else newConversation();configure();renderRadar();setInterval(()=>renderRadar(),30000); let installPrompt=null,installBtn=$('#installApp'),mobileInstallBtn=$('#installAppMobile'),networkState=$('#networkState');const showInstall=on=>{installBtn.style.display=on?'block':'none';mobileInstallBtn.classList.toggle('ready',on)}; function updateNetwork(){let online=navigator.onLine;networkState.textContent=online?'● В сети':'● Офлайн-режим';networkState.style.color=online?'#8fffc0':'#ffd27a'} addEventListener('online',updateNetwork);addEventListener('offline',updateNetwork);updateNetwork(); addEventListener('beforeinstallprompt',e=>{e.preventDefault();installPrompt=e;showInstall(true)}); installBtn.onclick=async()=>{if(installPrompt){installPrompt.prompt();await installPrompt.userChoice;installPrompt=null;showInstall(false)}else if(/iphone|ipad|ipod/i.test(navigator.userAgent))alert('В Safari нажмите «Поделиться» → «На экран Домой».')}; if(/iphone|ipad|ipod/i.test(navigator.userAgent)&&!matchMedia('(display-mode: standalone)').matches)showInstall(true); mobileInstallBtn.onclick=()=>installBtn.click(); addEventListener('appinstalled',()=>{showInstall(false);networkState.textContent='✓ ORBIT установлен'}); if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').then(reg=>{if(reg.waiting){installBtn.textContent='Обновить ORBIT';showInstall(true);installBtn.onclick=()=>reg.waiting.postMessage('SKIP_WAITING');mobileInstallBtn.textContent='Обновить ORBIT';mobileInstallBtn.onclick=installBtn.onclick}reg.addEventListener('updatefound',()=>{let w=reg.installing;w?.addEventListener('statechange',()=>{if(w.state==='installed'&&navigator.serviceWorker.controller){networkState.textContent='Доступно обновление — перезапустите ORBIT'}})})}).catch(()=>{})} navigator.serviceWorker?.addEventListener('controllerchange',()=>location.reload()); async function orbitTrackVisit(){try{let key='orbitAnonymousVisitorId',id=localStorage.getItem(key);if(!id){id=crypto.randomUUID().replace(/-/g,'');localStorage.setItem(key,id)}if(sessionStorage.getItem('orbitVisitSent'))return;let payload=JSON.stringify({visitorId:id,path:location.pathname,referrer:document.referrer});let res=await fetch('/api/track',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:payload,keepalive:true});if(res.ok)sessionStorage.setItem('orbitVisitSent','1')}catch(e){console.debug('ORBIT analytics unavailable',e)}} orbitTrackVisit();