⬅ 入口

📄 興和 文書管理

名前を選び、パスワードでログイン
🔑 パスワードをお忘れの方は、社長・事務にご連絡ください
🔄 新しい版があります
'; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } function printOne(id){ var d=DOCS.find(function(x){return x.id===id;}); if(!d)return; var rows=[['カテゴリ',d.category],['公開範囲',scopeJa(d.scope)],['版',d.version],['タグ',d.tags],['ファイル',d.fileName],['更新日',d.updatedAt],['登録者',d.uploadedBy],['メモ',d.note]]; var body=rows.map(function(r){ return ''+r[0]+''+esc(r[1]||'').replace(/\n/g,'
')+''; }).join(''); var html=''+esc(d.name)+''+ '

'+esc(d.name)+'

'+body+'
'+ ''; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&','<':'<','>':'>','"':'"'})[m]);} function showTab(v){ ['list','add','intake','photo','video','set'].forEach(x=>{var vw=document.getElementById('view-'+x);var tb=document.getElementById('tab-'+x);if(vw)vw.style.display=(x===v)?'block':'none';if(tb)tb.classList.toggle('on',x===v);}); if(v==='set'){ renderCats(); loadAudit(); renderMachineQR_(); renderStarterEdit_(); renderOfflineList_(); if(isPresident()){var c=document.getElementById('set-intake-card');if(c)c.style.display='';var s=document.getElementById('set-intake-role');if(s)s.value=DOCS_SETTINGS.aiIntakeRole||'president';} } } function render(){ const q=(document.getElementById('q').value||'').toLowerCase().trim(); const cf=document.getElementById('catfilter').value; const sf=(document.getElementById('scopefilter')||{}).value||''; const sort=(document.getElementById('sortby')||{}).value||'new'; const list=document.getElementById('list'); let arr=DOCS.filter(d=>{ if(cf&&d.category!==cf)return false; if(sf&&d.scope!==sf)return false; if(APP_FILTER){ const tg=String(d.tags==null?'':d.tags).toLowerCase(); if(tg.indexOf('app:'+APP_FILTER)<0)return false; } if(MACHINE_FILTER){ if(!isVideoDoc(d)||machineOf(d)!==MACHINE_FILTER) return false; } // v2.9: 機械の前のQRから来た時=その機械の動画だけ if(q){ const hay=((d.name||'')+' '+(d.tags||'')+' '+(d.category||'')+' '+(d.note||'')).toLowerCase(); if(hay.indexOf(q)<0)return false; } return true; }); if(sort==='name') arr.sort((a,b)=>String(a.name||'').localeCompare(String(b.name||''),'ja')); else if(sort==='cat') arr.sort((a,b)=>String(a.category||'').localeCompare(String(b.category||''),'ja')); else arr.sort((a,b)=>String(b.updatedAt||'').localeCompare(String(a.updatedAt||''))); window._filtered=arr; renderMachineBanner_(); renderStarter_(); // v2.9: 機械しぼり込みバナー+🔰初日メニュー var cnt=document.getElementById('count'); if(cnt) cnt.textContent=arr.length+' '+t('items'); if(!arr.length){ list.innerHTML='
'+t(MACHINE_FILTER?'mach_none':(APP_FILTER?'shelf_none':'no_docs'))+'
'; return; } list.innerHTML=arr.map(docCard).join(''); } /* v2.9: ② 機械しぼり込みバナー(QRで機械の前から来た時) */ function renderMachineBanner_(){ var b=document.getElementById('machine-banner'); if(!b) return; if(!MACHINE_FILTER){ b.style.display='none'; b.innerHTML=''; return; } b.style.display='block'; b.innerHTML='
' +'🏭 '+esc(MACHINE_FILTER)+' '+t('mach_of')+'' +'' +'
'; } function clearMachineFilter(){ MACHINE_FILTER=''; try{ if(/machine=/.test(location.hash)) history.replaceState(null,'',location.pathname+location.search); }catch(e){} render(); } /* v2.9: ③ 🔰初日メニュー(新人向け・必修動画を機械ごとに順番で)。しぼり込み・検索していない時だけ上部に出す */ function renderStarter_(){ var host=document.getElementById('starter-section'); if(!host) return; var q=(document.getElementById('q').value||'').trim(); var cf=document.getElementById('catfilter').value; var ids=starterIds(); if(!ids.length || q || cf || APP_FILTER || MACHINE_FILTER){ host.style.display='none'; host.innerHTML=''; return; } var byId={}; DOCS.forEach(function(d){ byId[d.id]=d; }); var picked=ids.map(function(id){ return byId[id]; }).filter(function(d){ return d&&isVideoDoc(d); }); if(!picked.length){ host.style.display='none'; host.innerHTML=''; return; } // 機械ごとにグループ化(順序は starter の並び順を保持) var groups=[], gmap={}; picked.forEach(function(d){ var m=machineOf(d)||t('mach_other'); if(!gmap[m]){ gmap[m]={m:m,items:[]}; groups.push(gmap[m]); } gmap[m].items.push(d); }); var body=groups.map(function(g){ var rows=g.items.map(function(d,i){ var seen=MY_PLAYS.has(d.id); return '
' +''+(i+1)+'' +''+esc(d.name)+'' +(seen?'✅ '+t('seen_badge')+'':'🆕 '+t('unseen_badge')+'') +'
'; }).join(''); return '
🏭 '+esc(g.m)+'
'+rows+'
'; }).join(''); host.style.display='block'; host.innerHTML='
' +'🔰 '+t('starter_title')+'' +'
'+t('starter_lead')+'
'+body+'
'; } function hasTrans(d){ try{ if(!d||!d.trans) return false; var o=(typeof d.trans==='string')?JSON.parse(d.trans):d.trans; return !!(o && (o.en||o.vi||o.zh||o.si||o.id||o.fil)); }catch(e){ return false; } } function docCard(d){ const scopeBdg = d.scope==='secret'?'🔒 '+t('scope_secret')+'' : d.scope==='admin'?''+t('scope_admin')+'' : ''+t('scope_all')+''; const tags=String(d.tags==null?'':d.tags).split(',').map(x=>x.trim()).filter(Boolean).map(x=>'#'+esc(x)+'').join(''); const photoBdg = hasTrans(d) ? '📷🌐 '+t('tab_photo')+'' : ''; const isVideo = !!(d.stepsJson && String(d.stepsJson).length>2); const videoBdg = isVideo ? '🎥 '+t('vm_badge')+'' : ''; const seenBdg = isVideo ? (MY_PLAYS.has(d.id) ? '✅ '+t('seen_badge')+'' : '🆕 '+t('unseen_badge')+'') : ''; // v2.9: 自分が見たか const offBdg = (isVideo && OFFLINE_IDS.has(d.id)) ? '📥 '+t('dl_saved_badge')+'' : ''; // v3.0: この端末に保存済み const adminBtns = (canManage() ? '' : '')+ (canDelete() ? '' : ''); // 編集=リーダー以上 / 削除=事務以上 // 🌐 本文のあるマニュアルは全て「母国語で読む」を出す(外国語UI時は強調)。原本は openBtn で従来どおり const transBtn = canTranslateDoc_(d) ? '' : ''; const openBtn = isVideo ? '' : ''; return '

'+esc(d.name)+'

'+ '
'+esc(d.category||'')+''+scopeBdg+ (d.version?''+esc(d.version)+'':'')+ (d.aiOk?'🤖 '+t('ai_on')+'':'')+videoBdg+seenBdg+offBdg+photoBdg+tags+'
'+ (d.note?'
'+esc(d.note)+'
':'')+ '
'+esc(d.fileName||'')+''+esc(d.updatedAt||'')+' / '+esc(d.uploadedBy||'')+'
'+ '
'+transBtn+openBtn+ ''+ ''+adminBtns+'
'; } /* 📷/📄 文書:🌐母国語で読む(本文をその場でAI翻訳)+原本を表示するモーダル */ let _docTransCur=null, _docTransTr={}; function parseTrans(d){ try{ return (typeof d.trans==='string')?JSON.parse(d.trans):(d.trans||{}); }catch(e){ return {}; } } function parsePhotos(d){ try{ var p=(typeof d.photos==='string')?JSON.parse(d.photos):(d.photos||[]); return Array.isArray(p)?p:[]; }catch(e){ return []; } } function showDocTrans(id){ const d=DOCS.find(x=>x.id===id); if(!d)return; _docTransCur=d; // サーバのtransを優先し、端末キャッシュ(過去にこの端末で翻訳した分)で穴埋め _docTransTr=Object.assign({}, (transCacheGet_(id)||{}), parseTrans(d)); const tr=_docTransTr; const photos=parsePhotos(d); // 原本写真サムネ:1枚目=本体fileUrl、2枚目以降=photos[].fileUrl(社外秘はfileUrl空→📂で開くよう案内) let imgs=''; const firstUrl=(d.scope!=='secret')?d.fileUrl:''; const allUrls=[]; if(firstUrl) allUrls.push(firstUrl); photos.forEach(function(p){ if(p&&p.fileUrl&&d.scope!=='secret') allUrls.push(p.fileUrl); }); if(allUrls.length){ imgs=allUrls.map(function(u,i){ return ''+ ''+ ''+t('photo_pages')+(i+1)+''; }).join(''); } else { imgs=''; } const langSel=''; const m=document.getElementById('editModal'); m.innerHTML='

📷 '+esc(d.name)+'

'+ '
'+esc(d.category||'')+'
'+ ''+ '
'+imgs+'
'+ '
'+t('photo_origone')+'
'+ '
'+langSel+'
'+ '
'+t('trans_note')+'
'+ '
'+ '
'+ '
'+ '
'; m.style.display='flex'; docTransLang(document.getElementById('dt-lang').value); } async function docTransLang(l){ const el=document.getElementById('dt-text'); if(!el||!_docTransCur)return; const d=_docTransCur; let body=docBody_(d); let didOcr=false; // 本文が無い(画像/PDFで未抽出)→ 端末キャッシュ→無ければその場でOCRして本文化 if(!body){ body = ocrCacheGet_(d.id) || ''; if(!body && docCanOcr_(d) && (d.fileId||d.fileUrl)){ el.textContent = t('ocr_running'); body = await ocrDocOnDemand_(d); if(body){ didOcr=true; ocrCacheSet_(d.id, body); if(!d.ocrText) d.ocrText=body; } } if(body){ _docTransTr.ja = body; } } if(l==='ja'){ el.textContent = body || _docTransTr.ja || ''; return; } // 既訳あり(原文と違う=ちゃんと翻訳済み)→ 即表示 if(_docTransTr[l] && _docTransTr[l]!==body){ el.textContent=_docTransTr[l]; return; } // それでも本文が無い(OCR不可の形式など)=翻訳不能 if(!body){ el.textContent = _docTransTr[l] || _docTransTr.ja || t('trans_nobody'); return; } // 🌐 初回オンデマンド翻訳(全6言語まとめて=以後この文書は全言語そろう) el.textContent = t('trans_first'); const transObj = Object.assign({ja:body}, _docTransTr); try{ await phTranslate(body, transObj); }catch(e){} _docTransTr = transObj; // 開いている間に言語を変えた場合の取り違え防止=現在の選択言語で表示 const sel=document.getElementById('dt-lang'); const cur=sel?sel.value:l; const showEl=document.getElementById('dt-text'); if(showEl) showEl.textContent = (cur==='ja'?body:transObj[cur]) || transObj[l] || body; // 端末キャッシュ(即時・GAS不要で2回目から速い) transCacheSet_(d.id, transObj); // 全員で共有:管理者=update(OCRした本文も正式保存)/それ以外=savetrans(本文が空の時だけ共有・GAS未対応なら無視・端末キャッシュは効く) const merged=JSON.stringify(transObj); const dd=DOCS.find(x=>x.id===d.id); if(dd){ dd.trans=merged; if(didOcr) dd.ocrText=body; } _docTransCur.trans=merged; try{ if(canManage()) docsGas('update',{id:d.id,fields: didOcr?{trans:merged,ocrText:body}:{trans:merged}}); else docsGas('savetrans', Object.assign({id:d.id,trans:transObj}, didOcr?{ocrText:body}:{})); }catch(e){} } /* 開く:非社外秘はDrive URL、社外秘はGAS経由でDL */ async function openDoc(id){ const d=DOCS.find(x=>x.id===id); if(!d)return; if(d.scope!=='secret' && d.fileUrl){ window.open(d.fileUrl,'_blank','noopener'); return; } try{ const r=await docsGas('getfile',{id}); if(!r||!r.ok){ alert((r&&r.error)||'取得できません'); return; } const bin=atob(r.dataB64); const a=new Uint8Array(bin.length); for(let i=0;iURL.revokeObjectURL(url),60000); }catch(e){ alert('取得エラー'); } } /* 登録 */ function fileToB64(file){ return new Promise((res,rej)=>{ const r=new FileReader(); r.onload=()=>res(String(r.result).split(',')[1]); r.onerror=rej; r.readAsDataURL(file); }); } let _aDropFile=null; function aFileShow(){ var f=(document.getElementById('a-file').files||[])[0]||_aDropFile; var el=document.getElementById('a-fname'); if(el) el.textContent=f?('📄 '+f.name):''; } function aDrop(files){ if(!files||!files.length) return; _aDropFile=files[0]; try{ var dt=new DataTransfer(); dt.items.add(files[0]); document.getElementById('a-file').files=dt.files; }catch(e){/* 一部ブラウザは代入不可→_aDropFileで拾う */} aFileShow(); } async function submitDoc(){ if(!canManage()){alert('権限がありません');return;} const name=document.getElementById('a-name').value.trim(); if(!name){alert('文書名を入力してください');return;} const file=document.getElementById('a-file').files[0]||_aDropFile; const scope=document.getElementById('a-scope').value; const stat=document.getElementById('a-stat'); stat.textContent='保存中…'; try{ let up={}; if(file){ if(file.size>18*1024*1024){ stat.textContent=''; alert('18MBを超えるファイルは現状アップできません(Drive直置き+リンク登録で対応予定)'); return; } const b64=await fileToB64(file); const u=await docsGas('upload',{dataB64:b64,mime:file.type,fileName:file.name,scope}); if(!u||!u.ok){ stat.textContent=''; alert((u&&u.error)||'アップロード失敗'); return; } up=u; } const r=await docsGas('add',{name,category:document.getElementById('a-cat').value,tags:document.getElementById('a-tags').value, scope,aiOk:document.getElementById('a-ai').checked,version:document.getElementById('a-ver').value,note:document.getElementById('a-note').value, fileId:up.fileId||'',fileUrl:up.fileUrl||'',fileMime:up.fileMime||'',fileName:up.fileName||''}); if(!r||!r.ok){ stat.textContent=''; alert('登録失敗'); return; } stat.textContent='✅ 登録しました'; document.getElementById('a-name').value='';document.getElementById('a-tags').value='';document.getElementById('a-ver').value=''; document.getElementById('a-note').value='';document.getElementById('a-file').value='';document.getElementById('a-ai').checked=false; _aDropFile=null; aFileShow(); await loadAll(); setTimeout(()=>{stat.textContent='';showTab('list');},700); }catch(e){ stat.textContent=''; alert('エラー:'+(e.message||e)); } } /* 編集 */ function editDoc(id){ const d=DOCS.find(x=>x.id===id); if(!d)return; const m=document.getElementById('editModal'); m.innerHTML='

✏️ '+esc(d.name)+'

'+ ''+ ''+ ''+ '
'+ '
'+ ''+ ''+ '
'+ '
'+ (isPresident()?'
':'')+ '
'; m.style.display='flex'; } function closeEdit(){ document.getElementById('editModal').style.display='none'; } async function saveEdit(id){ const fields={name:document.getElementById('e-name').value,category:document.getElementById('e-cat').value,tags:document.getElementById('e-tags').value, scope:document.getElementById('e-scope').value,version:document.getElementById('e-ver').value,note:document.getElementById('e-note').value, aiOk:document.getElementById('e-ai').checked?'true':'false'}; try{ const r=await docsGas('update',{id,fields}); if(!r||!r.ok){alert('更新失敗');return;} closeEdit(); await loadAll(); }catch(e){ alert('エラー'); } } async function delDoc(id){ if(!canDelete()){alert('削除は事務・社長のみです');return;} if(!confirm('この文書を削除(ゴミ箱へ)しますか?'))return; try{ await docsGas('del',{id}); await loadAll(); }catch(e){ alert('削除失敗'); } } async function purgeDoc(id){ if(!confirm('完全に削除します(元に戻せません・Driveのファイルも削除)。よろしいですか?'))return; try{ await docsGas('purge',{id}); closeEdit(); await loadAll(); }catch(e){ alert('削除失敗'); } } /* カテゴリ管理 */ function renderCats(){ const b=document.getElementById('catlist'); if(!b)return; b.innerHTML=CATS.map(c=>'
'+esc(c)+''+(canDelete()?'':'')+'
').join('')||'
'; } async function addCat(){ const v=document.getElementById('newcat').value.trim(); if(!v)return; try{ await docsGas('catadd',{name:v}); document.getElementById('newcat').value=''; await loadAll(); renderCats(); }catch(e){ alert('追加失敗'); } } async function delCat(name){ if(!canDelete()){alert('削除は事務・社長のみです');return;} if(!confirm(name+' を削除しますか?(文書は残ります)'))return; try{ await docsGas('catdel',{name}); await loadAll(); renderCats(); }catch(e){ alert('削除失敗'); } } async function loadAudit(){ const b=document.getElementById('auditlist'); if(!b)return; try{ const r=await docsGas('audit'); const a=(r&&r.audit)||[]; b.innerHTML=a.length?a.map(x=>'
'+esc(x.at)+' '+esc(x.user)+' '+esc(x.action)+' '+esc(x.target)+'
').join(''):'
'; }catch(e){ b.innerHTML='
'; } } /* ===================== 📎AI受付係(インテーク) ===================== */ /* B. ローカル赤旗チェック(端末内のみ・通信なし)。ヒットしたら停止。 */ const RED_FLAGS=[ {re:/マイナンバー|個人番号/, label:'マイナンバー'}, {re:/\d{12}/, label:'12桁の数字(個人番号の可能性)'}, {re:/源泉徴収/, label:'源泉徴収'}, {re:/給[与料]明細|賞与明細/, label:'給与・賞与明細'}, {re:/賃金台帳/, label:'賃金台帳'}, {re:/履歴書|職務経歴書/, label:'履歴書・職務経歴書'}, {re:/健康診断/, label:'健康診断'}, {re:/人事評価|査定/, label:'人事評価・査定'}, {re:/原価|仕入単価|見積原価/, label:'原価・仕入単価'}, {re:/価格戦略/, label:'価格戦略'}, {re:/社外秘|機密/, label:'社外秘・機密'}, {re:/NDA|守秘契約/i, label:'NDA・守秘契約'} ]; /* 「就業規則」「賃金規程」「退職金規程」は赤旗にしない=規程は全社OK */ function redFlagHits(text){ var s=String(text||''); var hits=[]; RED_FLAGS.forEach(function(f){ if(f.re.test(s) && hits.indexOf(f.label)<0) hits.push(f.label); }); return hits; } const AI_TXT_EXT=/\.(txt|csv|md|json|xml|log|tsv|html?|js|css|py)$/i; function isTextLike(file){ return AI_TXT_EXT.test(file.name||'') || /^text\//.test(file.type||'') || file.type==='application/json' || file.type==='text/csv'; } function isImage(file){ return /^image\//.test(file.type||'') || /\.(png|jpe?g|gif|webp)$/i.test(file.name||''); } function isPdf(file){ return file.type==='application/pdf' || /\.pdf$/i.test(file.name||''); } function readText(file){ return new Promise(function(res,rej){ var r=new FileReader(); r.onload=function(){res(String(r.result||''));}; r.onerror=rej; r.readAsText(file); }); } /* Excel/Word の中身をテキスト化(AI分別の精度UP・社長指示2026-06-25) */ function isXlsx(file){ return /\.(xlsx|xlsm|xls)$/i.test(file.name||'') || /spreadsheet|ms-excel/i.test(file.type||''); } function isDocx(file){ return /\.docx$/i.test(file.name||'') || /wordprocessingml/i.test(file.type||''); } function fileToArrayBuffer(file){ return new Promise(function(res,rej){ var r=new FileReader(); r.onload=function(){res(r.result);}; r.onerror=rej; r.readAsArrayBuffer(file); }); } async function extractXlsxText(file){ try{ if(typeof XLSX==='undefined') return ''; var ab=await fileToArrayBuffer(file); var wb=XLSX.read(ab,{type:'array'}); var out=[]; wb.SheetNames.forEach(function(n){ try{ var csv=XLSX.utils.sheet_to_csv(wb.Sheets[n]); if(csv && csv.replace(/[,\s]/g,'')) out.push('【シート: '+n+'】\n'+csv); }catch(e){} }); return out.join('\n\n').slice(0,20000); }catch(e){ return ''; } } async function extractDocxText(file){ try{ if(typeof mammoth==='undefined') return ''; var ab=await fileToArrayBuffer(file); var r=await mammoth.extractRawText({arrayBuffer:ab}); return String((r&&r.value)||'').slice(0,20000); }catch(e){ return ''; } } let AI_INTAKE=null; // {file, b64, mime} function aiStat(m){ var e=document.getElementById('ai-stat'); if(e)e.textContent=m||''; } /* 入口:選択/ドロップされたファイルを振り分け(1件→単票、複数→一括・社長指示2026-06-25) */ function aiFilesPicked(fileList){ if(!canUseIntake()){ alert('権限がありません'); return; } const files=Array.prototype.slice.call(fileList||[]); if(!files.length){ aiStat(t('intake_nofile')); return; } document.getElementById('ai-result').innerHTML=''; if(files.length===1) aiSingleIntake(files[0]); else aiBatchRun(files); } function aiIntakeStart(){ var el=document.getElementById('ai-file'); aiFilesPicked(el?el.files:[]); } /* 1ファイルをAIで分別(読込→赤旗→AI判定)。{ok,obj,b64,readMode}/{redflag,hits}/{err} を返す */ async function aiClassifyFile(file, statFn){ if(file.size>18*1024*1024){ return {err:true, big:true}; } if(statFn) statFn(t('intake_checking')); let sample=file.name; try{ if(isTextLike(file)) sample+=' '+(await readText(file)).slice(0,20000); else if(isXlsx(file)) sample+=' '+(await extractXlsxText(file)); else if(isDocx(file)) sample+=' '+(await extractDocxText(file)); }catch(e){} const hits=redFlagHits(sample); if(hits.length) return {redflag:true, hits:hits}; if(statFn) statFn(t('intake_asking')); try{ let userContent, readMode='name', b64='', localBody=''; if(isTextLike(file)){ const body=(await readText(file)).slice(0,20000); localBody=body; userContent='【ファイル名】'+file.name+'\n【本文(先頭抜粋)】\n'+body; readMode='text'; } else if(isXlsx(file)){ const body=await extractXlsxText(file); localBody=body||''; userContent='【ファイル名】'+file.name+'\n【Excelの中身(抜粋)】\n'+(body||'(空・読めず)'); readMode='xlsx'; } else if(isDocx(file)){ const body=await extractDocxText(file); localBody=body||''; userContent='【ファイル名】'+file.name+'\n【Wordの中身(抜粋)】\n'+(body||'(空・読めず)'); readMode='docx'; } else if(isImage(file)){ b64=await fileToB64(file); userContent=[{type:'image',source:{type:'base64',media_type:(file.type||'image/png'),data:b64}},{type:'text',text:'【ファイル名】'+file.name+'\n上の画像の中身を読み、基準に照らして判定してください。本文はocrTextに全文書き出すこと。'}]; readMode='vision'; } else if(isPdf(file)){ b64=await fileToB64(file); userContent=[{type:'document',source:{type:'base64',media_type:'application/pdf',data:b64}},{type:'text',text:'【ファイル名】'+file.name+'\n上のPDFの中身を読み、基準に照らして判定してください。本文はocrTextに全文書き出すこと。'}]; readMode='vision'; } else { userContent='【ファイル名】'+file.name+'\n(このファイルは中身を自動で読めません。ファイル名だけで暫定判定し、reasonに「中身未読・人の確認必須」と必ず明記し、canRegister=false / scope=admin / aiOk=false にしてください)'; readMode='name'; } const res=await gas('ai',{app:'docs',system:INTAKE_SYSTEM(), max_tokens:8000, messages:[{role:'user',content:userContent}]}); const obj=extractIntakeJSON((res&&res.reply)||''); if(!obj) return {err:true}; if(!obj.title) obj.title=file.name; /* 📖 本文=AI検索の根拠。text/xlsx/docxはローカル抽出が確実なのでそれを優先、PDF/画像はAIのOCR結果 */ if(localBody && localBody.length>(obj.ocrText||'').length) obj.ocrText=localBody; obj.ocrText=String(obj.ocrText||'').slice(0,40000); return {ok:true, obj:obj, b64:b64, readMode:readMode}; }catch(e){ return {err:true}; } } /* 単票(1ファイル):従来どおり確認カード */ async function aiSingleIntake(file){ const r=await aiClassifyFile(file, aiStat); if(r.big){ aiStat(''); alert('18MBを超えるファイルは現状アップできません'); return; } if(r.redflag){ aiStat(''); showRedFlag(r.hits, file); return; } if(r.err){ aiStat(t('intake_err')); return; } aiStat(''); AI_INTAKE={file:file, readMode:r.readMode, obj:r.obj}; AI_INTAKE_B64=r.b64||''; renderIntakeResult(r.obj); } /* 一括(複数ファイル):全部AIで分別→確認リスト→まとめて保存 */ let BATCH=[]; async function aiBatchRun(files){ BATCH=[]; for(let i=0;ib.obj&&!b.skip).length; let h='

'+t('batch_title')+'('+BATCH.length+')

'+ '
'+t('batch_lead')+'
'; BATCH.forEach((b,i)=>{ if(b.redflag){ h+='
⚠ '+esc(b.file.name)+'
'+esc(b.hits.join('、'))+' = '+t('batch_redflag')+'
'; return; } if(b.err){ h+='
'+esc(b.file.name)+'
'+t('batch_aierr')+'
'; return; } const o=b.obj; const catOpts=CATS.map(c=>''+esc(c)+'').join('')||''; const scOpts=['all','admin','secret'].map(s=>'').join(''); h+='
'+ ''+ '
'+esc(o.reason)+'
'+ ''+ '
'+ '
'+ ''+ ''+ '
'; }); h+='
'+ '
'; box.innerHTML=h; } function batchToggle(i,checked){ if(BATCH[i]) BATCH[i].skip=!checked; renderBatchResults(); } function aiBatchReject(){ BATCH=[]; document.getElementById('ai-result').innerHTML=''; const el=document.getElementById('ai-file'); if(el)el.value=''; } async function aiBatchSaveAll(){ if(!canManage()){ alert('権限がありません'); return; } const items=BATCH.filter(b=>b.obj&&!b.skip); if(!items.length){ alert(t('batch_none')); return; } const st=document.getElementById('batch-stat'); let ok=0; for(let k=0;k{ showTab('list'); },900); } let AI_INTAKE_B64=''; function INTAKE_SYSTEM(){ const cats=(CATS&&CATS.length)?CATS.join(' / '):'規程・ルール / マニュアル・手順 / 様式・テンプレート / 議事録・社内通達 / 機械説明書 / 安全・品質 / その他'; return [ 'あなたは興和化学(プラスチック加工・パトミル事業)の文書受付係です。社内文書を「文書取り扱い基準書」に照らして仕分けます。', '【基準】', '🚫入れない(canRegister=false):個人情報(マイナンバー/個人別の給与/健康診断/履歴書/人事評価)・経営機密(詳細な原価/価格戦略/NDA/未公開のM&A)。', '🟢全社オープン(scope=all, aiOk=true):就業規則・各種規程・安全マニュアル・作業手順・様式・社内通達。', '🟡社内実務(scope=admin, aiOk=true):機械説明書・議事録・管理資料。', '🔵見せるが渡さない(scope=admin, aiOk=false):取引先名など固有名詞が入った資料。', '迷ったら scope=admin・aiOk=false(安全側)。', '【カテゴリ候補】'+cats+'(最も近いものを選ぶ。無ければ「その他」)。', '【本文(ocrText)】ファイルの中身が読める場合は、本文を日本語のまま全文 ocrText に書き出す(要約しない・順番どおり・表は「項目: 値」の箇条書き・読めない箇所は[不明])。これが後のAI検索の根拠になる。canRegister=false のときは ocrText は空でよい。', '【出力】必ず次のJSONだけを返す(前後に文章を付けない):', '{"canRegister":true/false,"reason":"理由(日本語・1〜2文)","title":"タイトル案","category":"カテゴリ名","scope":"all|admin|secret","aiOk":true/false,"tags":"カンマ区切り","ocrText":"本文の全文"}', 'canRegister=false のときも reason に理由を必ず書く。' ].join('\n'); } /* AI応答から安全にJSON抽出 */ function extractIntakeJSON(s){ if(!s) return null; var txt=String(s); var m=txt.match(/```(?:json)?\s*([\s\S]*?)```/i); if(m) txt=m[1]; var a=txt.indexOf('{'), b=txt.lastIndexOf('}'); if(a<0||b<0||b=0?o.scope:'admin'), aiOk: o.aiOk===true || String(o.aiOk)==='true', tags:String(o.tags||''), ocrText:String(o.ocrText||'') }; }catch(e){ return null; } } /* D. 判定カード(各項目を社長が手で直せる) */ function renderIntakeResult(o){ const box=document.getElementById('ai-result'); const catOpts=CATS.map(c=>''+esc(c)+'').join('')||''; const yn=o.canRegister?('✅ '+t('res_yes')+''):('🚫 '+t('res_no')+''); box.innerHTML='
'+ '

'+t('intake_result')+'

'+ '
'+yn+'
'+ '
'+esc(o.reason)+'
'+ (!o.canRegister?'
'+t('intake_override')+'
':'')+ ''+ '
'+ ''+ '
'+ '
'+ ''+ '
'+ ''+ '
'+ '
'+ '
'; } function aiIntakeReject(){ document.getElementById('ai-result').innerHTML=''; document.getElementById('ai-file').value=''; AI_INTAKE=null; AI_INTAKE_B64=''; } async function aiIntakeSave(){ if(!canManage()){ alert('権限がありません'); return; } if(!AI_INTAKE||!AI_INTAKE.file){ return; } const canReg=document.getElementById('ai-canreg').checked; if(!canReg && !confirm('AIは「入れない方がよい」と判定しています。それでも登録しますか?')) return; const name=(document.getElementById('ai-title').value||'').trim(); if(!name){ alert(t('f_name')); return; } const scope=document.getElementById('ai-scope').value; const st=document.getElementById('ai-save-stat'); st.textContent='保存中…'; try{ const b64=AI_INTAKE_B64 || await fileToB64(AI_INTAKE.file); const u=await docsGas('upload',{dataB64:b64,mime:AI_INTAKE.file.type,fileName:AI_INTAKE.file.name,scope}); if(!u||!u.ok){ st.textContent=''; alert((u&&u.error)||'アップロード失敗'); return; } const r=await docsGas('add',{name,category:document.getElementById('ai-cat').value,tags:document.getElementById('ai-tags').value, scope,aiOk:document.getElementById('ai-aiok').checked,version:'',note:'📎AI受付係で登録', fileId:u.fileId||'',fileUrl:u.fileUrl||'',fileMime:u.fileMime||'',fileName:u.fileName||'', ocrText:String((AI_INTAKE.obj&&AI_INTAKE.obj.ocrText)||'').slice(0,40000)}); if(!r||!r.ok){ st.textContent=''; alert('登録失敗'); return; } st.textContent=t('intake_saved'); aiIntakeReject(); await loadAll(); setTimeout(()=>{ showTab('list'); },700); }catch(e){ st.textContent=''; alert('エラー:'+(e.message||e)); } } /* A. 社長:受付係を使える人 設定保存 */ async function saveIntakeRole(){ if(!isPresident()){ alert('権限がありません'); return; } const v=document.getElementById('set-intake-role').value; const st=document.getElementById('set-intake-stat'); st.textContent='…'; try{ const r=await docsGas('setSetting',{key:'aiIntakeRole',value:v}); if(!r||!r.ok){ st.textContent=''; alert((r&&r.error)||'保存失敗'); return; } DOCS_SETTINGS.aiIntakeRole=v; applyTabVisibility(); st.textContent=t('set_intake_saved'); setTimeout(()=>{st.textContent='';},1500); }catch(e){ st.textContent=''; alert('保存エラー'); } } function showRedFlag(hits, file){ const box=document.getElementById('ai-result'); box.innerHTML='
'+ '

'+t('redflag_title')+'

'+ '
'+esc(t('redflag_msg').replace('{hit}',hits.join('、')))+'
'+ '
'+ ''+ '
'+ '
'; AI_INTAKE={file:file, readMode:'redflag'}; } /* 赤旗ヒット時:AIに送らず、通常の登録タブへファイル名を引き継いで手動登録 */ function redFlagManual(){ const f=AI_INTAKE&&AI_INTAKE.file; aiIntakeReject(); if(canManage()){ showTab('add'); if(f){ const n=document.getElementById('a-name'); if(n&&!n.value) n.value=f.name.replace(/\.[^.]+$/,''); } // ファイルは安全のため自動添付しない(社長が手で選ぶ) alert('個人情報・機密の可能性があるため、AIには送りませんでした。登録タブで内容を確認し、公開範囲を「社外秘/管理者のみ」にしてご登録ください。'); } } /* ===================== 📷 写真取り込み(OCR+7言語翻訳) ===================== */ const PHOTO_LANGS=['ja','en','vi','si','id','fil','zh']; const PHOTO_LANG_NAMES={ja:'日本語',en:'英語(English)',vi:'ベトナム語(Tiếng Việt)',si:'シンハラ語(සිංහල)',id:'インドネシア語(Bahasa Indonesia)',fil:'フィリピン語(Filipino)',zh:'中国語(中文)'}; let PHOTO_FILES=[]; // 選択された画像File配列(複数ページ) let PHOTO_IMGS=[]; // 圧縮後の {b64, mime} 配列 let PHOTO_RESULT=null; // {ocrText,title,category,scope,aiOk,tags,canRegister,reason,trans} function phStat(m){ var e=document.getElementById('ph-stat'); if(e)e.textContent=m||''; } function phPick(ev){ if(!canUseIntake()){ alert('権限がありません'); return; } const files=Array.prototype.slice.call((ev&&ev.target&&ev.target.files)||[]); files.forEach(function(f){ if(/^image\//.test(f.type)||/\.(png|jpe?g|gif|webp|heic|heif)$/i.test(f.name||'')) PHOTO_FILES.push(f); }); renderPhThumbs(); } function phClear(){ PHOTO_FILES=[]; PHOTO_IMGS=[]; PHOTO_RESULT=null; document.getElementById('ph-file').value=''; document.getElementById('ph-result').innerHTML=''; phStat(''); renderPhThumbs(); } function renderPhThumbs(){ const box=document.getElementById('ph-thumbs'); const acts=document.getElementById('ph-actions'); if(!box) return; box.innerHTML=PHOTO_FILES.map(function(f,i){ const url=URL.createObjectURL(f); return '
'+ ''+ ''+(i+1)+''+ ''+ '
'; }).join(''); if(acts) acts.style.display=PHOTO_FILES.length?'block':'none'; if(PHOTO_FILES.length){ phStat(PHOTO_FILES.length+t('photo_selected')); } else phStat(''); } function phRemove(i){ PHOTO_FILES.splice(i,1); renderPhThumbs(); } /* 画像を長辺1600px・JPEG品質0.8に圧縮(端末内・通信なし) */ function compressImage(file){ return new Promise(function(resolve,reject){ const url=URL.createObjectURL(file); const img=new Image(); img.onload=function(){ try{ const max=1600; let w=img.naturalWidth||img.width, h=img.naturalHeight||img.height; if(w>max||h>max){ if(w>=h){ h=Math.round(h*max/w); w=max; } else { w=Math.round(w*max/h); h=max; } } const cv=document.createElement('canvas'); cv.width=w; cv.height=h; const cx=cv.getContext('2d'); cx.fillStyle='#fff'; cx.fillRect(0,0,w,h); cx.drawImage(img,0,0,w,h); const dataUrl=cv.toDataURL('image/jpeg',0.8); URL.revokeObjectURL(url); resolve({ b64:dataUrl.split(',')[1], mime:'image/jpeg' }); }catch(e){ URL.revokeObjectURL(url); reject(e); } }; img.onerror=function(){ URL.revokeObjectURL(url); reject(new Error('img load failed')); }; img.src=url; }); } function PHOTO_OCR_SYSTEM(){ const cats=(CATS&&CATS.length)?CATS.join(' / '):'規程・ルール / マニュアル・手順 / 標準作業書 / SDS(安全データシート) / 機械説明書 / その他'; return [ 'あなたは興和化学(プラスチック加工・パトミル事業)の文書受付係兼OCR担当です。', '与えられた画像(書類を撮影したもの・複数ページの場合あり)の文字を正確に読み取り、基準書に照らして仕分けます。', '【基準】', '🚫入れない(canRegister=false):個人情報(マイナンバー/個人別の給与/健康診断/履歴書/人事評価)・経営機密(詳細な原価/価格戦略/NDA/未公開のM&A)。', '🟢全社オープン(scope=all, aiOk=true):SDS・安全マニュアル・標準作業書・作業手順・各種規程・機械銘板。', '🟡社内実務(scope=admin, aiOk=true):機械説明書・議事録・管理資料。迷ったら scope=admin・aiOk=false。', '【カテゴリ候補】'+cats+'(最も近いものを選ぶ。SDSは「SDS(安全データシート)」、作業書は「標準作業書」、機械銘板等は「機械説明書」を優先)。', '【OCR】全ページの本文を日本語のまま順に結合して ocrText に入れる(ページ区切りは改行2つ)。読めない箇所は[不明]と記す。原文が日本語以外でも、まず読み取った原文を ocrText に書く。', '【出力】必ず次のJSONだけを返す(前後に文章・コードフェンス無し):', '{"ocrText":"全ページ結合の本文","title":"タイトル案","category":"カテゴリ名","scope":"all|admin|secret","aiOk":true/false,"tags":"カンマ区切り","canRegister":true/false,"reason":"理由(日本語1〜2文)"}' ].join('\n'); } function PHOTO_TRANS_SYSTEM(){ return [ 'あなたは製造業の現場文書の翻訳者です。日本語の文書本文を、外国人実習生が母国語で安全に理解できるよう翻訳します。', 'SDSや作業書では「危険・取扱い・応急処置・禁止事項」が正確に伝わることを最優先にしてください。意訳しすぎず、専門用語は分かりやすく。', '指定された言語へ翻訳し、必ず次のJSONだけを返す(前後に文章・コードフェンス無し):', '{"trans":{"":"訳文", ...}}', 'キーは指定された言語コードのみ。ja は翻訳不要(入っていれば原文のまま)。' ].join('\n'); } /* JSON堅牢抽出(コードフェンス/前後文除去) */ function extractJSONObj(s){ if(!s) return null; var txt=String(s); var m=txt.match(/```(?:json)?\s*([\s\S]*?)```/i); if(m) txt=m[1]; var a=txt.indexOf('{'), b=txt.lastIndexOf('}'); if(a<0||b<0||b=0?ocrObj.scope:'admin'), aiOk:(ocrObj.aiOk===true||String(ocrObj.aiOk)==='true'), tags:String(ocrObj.tags||''), canRegister:(ocrObj.canRegister===true||String(ocrObj.canRegister)==='true'), reason:String(ocrObj.reason||'') }; // 3) 翻訳(別リクエスト・max_tokens対策で OCR と分離)。失敗しても保存はできるようにフォールバック。 phStat(t('photo_translating')); const trans={ja:base.ocrText}; await phTranslate(base.ocrText, trans); base.trans=trans; PHOTO_RESULT=base; phStat(''); renderPhotoResult(base); }catch(e){ phStat(t('photo_err')); } } /* 翻訳:6言語(ja以外)を 3言語ずつ2回に分割=1回の応答が切れにくい堅牢実装。 失敗した言語は ja(原文) フォールバックで埋める(穴を作らない)。 */ async function phTranslate(jaText, transOut){ const targetGroups=[['en','vi','si'],['id','fil','zh']]; const body=String(jaText||'').slice(0,6000); // 1リクエストの本文も上限 for(let g=0;g''+esc(c)+'').join('')||''; const yn=o.canRegister?('✅ '+t('res_yes')+''):('🚫 '+t('res_no')+''); const langSel=''; const thumbs=PHOTO_IMGS.map(function(im,i){ return '
'+ ''+t('photo_pages')+(i+1)+'
'; }).join(''); box.innerHTML='
'+ '

'+t('intake_result')+'

'+ '
'+yn+'
'+ '
'+esc(o.reason)+'
'+ (!o.canRegister?'
'+t('intake_override')+'
':'')+ ''+ '
'+ ''+ '
'+ '
'+ ''+ /* 原本写真 */ ''+ '
'+thumbs+'
'+ '
'+t('photo_origone')+'
'+ /* 言語切替プレビュー(参考訳) */ '
'+langSel+'
'+ '
'+t('ref_translation')+'
'+ '
'+ '
'+ ''+ '
'+ '
'+ '
'; phPrevLang(document.getElementById('ph-prev-lang').value); } function phPrevLang(l){ const el=document.getElementById('ph-prev-text'); if(!el||!PHOTO_RESULT)return; const tr=PHOTO_RESULT.trans||{}; el.textContent = (l==='ja' ? (PHOTO_RESULT.ocrText||tr.ja||'') : (tr[l]||PHOTO_RESULT.ocrText||'')); } function phReject(){ phClear(); } async function phSave(){ if(!canManage()){ alert('権限がありません'); return; } if(!PHOTO_RESULT||!PHOTO_IMGS.length){ return; } const canReg=document.getElementById('ph-canreg').checked; if(!canReg && !confirm('AIは「入れない方がよい」と判定しています。それでも登録しますか?')) return; const name=(document.getElementById('ph-title').value||'').trim(); if(!name){ alert(t('f_name')); return; } const scope=document.getElementById('ph-scope').value; const st=document.getElementById('ph-save-stat'); st.textContent=t('photo_save_stat'); try{ // 全ページ画像をDriveへ保存(1枚目=本体、2枚目以降=photos) const ups=[]; for(let i=0;i{ showTab('list'); },800); }catch(e){ st.textContent=''; alert('エラー:'+(e.message||e)); } } /* 音声入力 */ function voiceTo(targetId){ const SR=window.SpeechRecognition||window.webkitSpeechRecognition; if(!SR){alert('この端末は音声入力に対応していません');return;} const r=new SR(); r.lang=({ja:'ja-JP',en:'en-US',vi:'vi-VN',zh:'zh-CN',id:'id-ID',fil:'fil-PH',si:'si-LK'})[LANG]||'ja-JP'; r.interimResults=false; r.onresult=function(e){ const tx=e.results[0][0].transcript; const el=document.getElementById(targetId); if(el){ el.value=(el.value?el.value+' ':'')+tx; el.dispatchEvent(new Event('input')); } }; r.start(); } function voiceSearch(){ voiceTo('q'); } /* ===================== QR生成器(オフライン・外部依存なし) ===================== qrcode-generator (Kazuhiko Arase, MIT License) のコンパクト実装。 外部CDN/APIに一切依存せず、端末内でQRを描画する。 */ var qrcode=(function(){ function QR8bitByte(d){this.mode=4;this.data=d;this.parsedData=[];for(var i=0,l=this.data.length;i0x10000){b[0]=0xF0|((c&0x1C0000)>>>18);b[1]=0x80|((c&0x3F000)>>>12);b[2]=0x80|((c&0xFC0)>>>6);b[3]=0x80|(c&0x3F);} else if(c>0x800){b[0]=0xE0|((c&0xF000)>>>12);b[1]=0x80|((c&0xFC0)>>>6);b[2]=0x80|(c&0x3F);} else if(c>0x80){b[0]=0xC0|((c&0x7C0)>>>6);b[1]=0x80|(c&0x3F);}else{b[0]=c;}this.parsedData.push(b);} this.parsedData=Array.prototype.concat.apply([],this.parsedData); if(this.parsedData.length!=this.data.length){this.parsedData.unshift(191);this.parsedData.unshift(187);this.parsedData.unshift(239);}} QR8bitByte.prototype={getLength:function(){return this.parsedData.length;},write:function(buf){for(var i=0,l=this.parsedData.length;i=7)this.setupTypeNumber(test); if(this.dataCache==null)this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList); this.mapData(this.dataCache,mask);}, setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue; this.modules[row+r][col+c]=(0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4);}}}, getBestMaskPattern:function(){var min=0,pat=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lost=QRUtil.getLostPoint(this);if(i==0||min>lost){min=lost;pat=i;}}return pat;}, setupTimingPattern:function(){for(var r=8;r>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}}, setupTypeInfo:function(test,mask){var data=(this.errorCorrectLevel<<3)|mask;var bits=QRUtil.getBCHTypeInfo(data); for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6)this.modules[i][8]=mod;else if(i<8)this.modules[i+1][8]=mod;else this.modules[this.moduleCount-15+i][8]=mod;} for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8)this.modules[8][this.moduleCount-i-1]=mod;else if(i<9)this.modules[8][15-i-1+1]=mod;else this.modules[8][15-i-1]=mod;} this.modules[this.moduleCount-8][8]=(!test);}, mapData:function(data,mask){var inc=-1,row=this.moduleCount-1,bitIdx=7,byteIdx=0; for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIdx>>bitIdx)&1)==1); if(QRUtil.getMask(mask,row,col-c))dark=!dark;this.modules[row][col-c]=dark;bitIdx--;if(bitIdx==-1){byteIdx++;bitIdx=7;}}}row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}}; QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11; QRCodeModel.createData=function(tn,el,dl){var rs=QRRSBlock.getRSBlocks(tn,el);var buf=new QRBitBuffer(); for(var i=0;itotal*8)throw new Error("code length overflow. ("+buf.getLengthInBits()+">"+total*8+")"); if(buf.getLengthInBits()+4<=total*8)buf.put(0,4);while(buf.getLengthInBits()%8!=0)buf.putBit(false); while(true){if(buf.getLengthInBits()>=total*8)break;buf.put(QRCodeModel.PAD0,8);if(buf.getLengthInBits()>=total*8)break;buf.put(QRCodeModel.PAD1,8);} return QRCodeModel.createBytes(buf,rs);}; QRCodeModel.createBytes=function(buf,rs){var off=0,maxDc=0,maxEc=0,dcData=new Array(rs.length),ecData=new Array(rs.length); for(var r=0;r=0)?modPoly.get(modIdx):0;}} var totalCodeCount=0;for(var i=0;i=0)g^=(QRUtil.G15<<(QRUtil.getBCHDigit(g)-QRUtil.getBCHDigit(QRUtil.G15)));return((d<<10)|g)^QRUtil.G15_MASK;}, getBCHTypeNumber:function(d){var g=(d<<12);while(QRUtil.getBCHDigit(g)-QRUtil.getBCHDigit(QRUtil.G18)>=0)g^=(QRUtil.G18<<(QRUtil.getBCHDigit(g)-QRUtil.getBCHDigit(QRUtil.G18)));return(d<<12)|g;}, getBCHDigit:function(d){var n=0;while(d!=0){n++;d>>>=1;}return n;}, getPatternPosition:function(tn){return QRUtil.PATTERN_POSITION_TABLE[tn-1];}, getMask:function(p,i,j){switch(p){case 0:return(i+j)%2==0;case 1:return i%2==0;case 2:return j%3==0;case 3:return(i+j)%3==0;case 4:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case 5:return(i*j)%2+(i*j)%3==0;case 6:return((i*j)%2+(i*j)%3)%2==0;case 7:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad mask:"+p);}}, getErrorCorrectPolynomial:function(ec){var a=new QRPolynomial([1],0);for(var i=0;i5)lost+=(3+same-5);} for(var r=0;r=256)n-=255;return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)}; for(var i=0;i<8;i++)QRMath.EXP_TABLE[i]=1<>>(7-i%8))&1)==1;},put:function(num,len){for(var i=0;i>>(len-i-1))&1)==1);},getLengthInBits:function(){return this.length;},putBit:function(bit){var bi=Math.floor(this.length/8);if(this.buffer.length<=bi)this.buffer.push(0);if(bit)this.buffer[bi]|=(0x80>>>(this.length%8));this.length++;}}; /* 公開API:qrcode(text) → {modules,count}。typeNumberは長さから自動選択(ErrorCorrect=M) */ return function(text){ var el=QRErrorCorrectLevel.M; for(var tn=1;tn<=40;tn++){ try{ var m=new QRCodeModel(tn,el); m.addData(text); m.make(); return {count:m.getModuleCount(),isDark:function(r,c){return m.isDark(r,c);}}; } catch(e){ /* このtypeNumberに収まらない→次へ */ } } throw new Error('QR: data too long'); }; })(); /* QR → SVG文字列(外部依存なし・印刷でもくっきり) */ function qrSvg(text, px){ px = px || 200; var qr = qrcode(text); var n = qr.count; var quiet = 4; var total = n + quiet*2; var cell = px/total; var rects=''; for(var r=0;r'; }}} return ''+ ''+rects+''; } /* QRが指す先:全社公開(scope=all)+fileUrlあり→Drive直リンク/それ以外→アプリのディープリンク(要ログイン) */ function qrTargetUrl(d){ if(d && d.scope==='all' && d.fileUrl) return d.fileUrl; return 'https://kowa-docs.pages.dev/#doc='+encodeURIComponent(d?d.id:''); } function qrIsDirect(d){ return !!(d && d.scope==='all' && d.fileUrl); } /* A. 1文書のQR表示モーダル */ function showQR(id){ var d=DOCS.find(function(x){return x.id===id;}); if(!d) return; var url=qrTargetUrl(d); var direct=qrIsDirect(d); var m=document.getElementById('editModal'); var svg; try{ svg=qrSvg(url, 240); }catch(e){ svg='
QR生成に失敗しました
'; } m.innerHTML='
'+ '

📱 '+t('qr_title')+'

'+ '
'+t('qr_lead')+'
'+ '
'+esc(d.name)+'
'+ '
'+esc(d.category||'')+'
'+ '
'+svg+'
'+ '
'+(direct?'✅ '+t('qr_direct'):t('qr_deeplink'))+'
'+ '
'+ ''+ '
'+ '
'; m.style.display='flex'; } /* 1文書のQRを印刷(クリーンHTML+window.print) */ function printOneQR(id){ var d=DOCS.find(function(x){return x.id===id;}); if(!d) return; printQRLabels([d]); } /* B. QRラベル一括印刷モーダル(現在の絞り込み対象から選ぶ) */ function openQRLabels(){ var arr=window._filtered||DOCS; var m=document.getElementById('editModal'); var rows=arr.map(function(d){ return ''; }).join('')||'
'+t('no_docs')+'
'; m.innerHTML='
'+ '

📱 '+t('qr_labels_title')+'

'+ '
'+t('qr_labels_lead')+'
'+ '
'+ ''+ ''+ '
'+ '
'+rows+'
'+ '
'+ ''+ '
'+ '
'; m.style.display='flex'; updQRSel(); } function qrSelAll(on){ document.querySelectorAll('.qrlbl-chk').forEach(function(c){ c.checked=on; }); updQRSel(); } function updQRSel(){ var n=document.querySelectorAll('.qrlbl-chk:checked').length; var e=document.getElementById('qr-sel-count'); if(e)e.textContent=t('qr_labels_sel')+': '+n; } function printSelectedQR(){ var ids=Array.from(document.querySelectorAll('.qrlbl-chk:checked')).map(function(c){return c.value;}); if(!ids.length){ alert(t('qr_labels_none_sel')); return; } var arr=ids.map(function(id){return DOCS.find(function(x){return x.id===id;});}).filter(Boolean); printQRLabels(arr); } /* QRラベルシートを新ウィンドウで印刷(名刺〜カードサイズのグリッド) */ function printQRLabels(arr){ if(!arr||!arr.length) return; var cells=arr.map(function(d){ var url=qrTargetUrl(d); var svg; try{ svg=qrSvg(url,150); }catch(e){ svg=''; } return '
'+ '
'+esc(d.name)+'
'+ '
'+esc(d.category||'')+'
'+ '
'+svg+'
'+ '
'+esc(t('qr_scan_hint'))+(qrIsDirect(d)?'':' ⚠')+'
'+ '
'; }).join(''); var html=''+esc(t('qr_label_sheet'))+''+ '

📱 '+esc(t('qr_label_sheet'))+'

'+ '
'+pNow()+' / '+arr.length+' '+t('items')+' | ⚠ = '+t('qr_deeplink')+'
'+ '
'+cells+'
'+ ''+ ''; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } /* ===== v2.9 ② 機械QR:機械の前に貼る=読むとその機械の動画だけ ===== */ function machineQrUrl(m){ return 'https://kowa-docs.pages.dev/#machine='+encodeURIComponent(m); } function renderMachineQR_(){ var host=document.getElementById('machineqr-list'); if(!host) return; var machines=videoMachines(); if(!machines.length){ host.innerHTML='
'+t('machqr_empty')+'
'; return; } host.innerHTML=machines.map(function(m){ var cnt=DOCS.filter(function(d){ return isVideoDoc(d)&&machineOf(d)===m; }).length; var svg=''; try{ svg=qrSvg(machineQrUrl(m),120); }catch(e){ svg=''; } return '
' +'
'+svg+'
' +'
🏭 '+esc(m)+'
' +'
🎥 '+cnt+' '+t('items')+'
' +'' +'
'; }).join(''); } function printMachineQR(m){ var url=machineQrUrl(m); var svg=''; try{ svg=qrSvg(url,300); }catch(e){} var cnt=DOCS.filter(function(d){ return isVideoDoc(d)&&machineOf(d)===m; }).length; var html=''+esc(m)+' QR' +'
🏭 '+esc(m)+'
' +'
'+esc(t('machqr_poster'))+'
' +'
'+svg+'
' +'
📱 '+esc(t('machqr_scan'))+'
' +'
🎥 '+cnt+' '+t('items')+'
' +'' +''; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } /* ===== v2.9 ③ 🔰初日メニュー編集:必修動画に✓+▲▼で順番 ===== */ let STARTER_DRAFT=null; function renderStarterEdit_(){ var host=document.getElementById('starter-edit'); if(!host) return; if(STARTER_DRAFT===null) STARTER_DRAFT=starterIds().slice(); var vids=DOCS.filter(isVideoDoc); if(!vids.length){ host.innerHTML='
'+t('starter_empty')+'
'; return; } // 選択済み(順序保持)→ 未選択(機械順) var byId={}; vids.forEach(function(d){ byId[d.id]=d; }); var sel=STARTER_DRAFT.filter(function(id){ return byId[id]; }); var rest=vids.filter(function(d){ return sel.indexOf(d.id)<0; }) .sort(function(a,b){ return (machineOf(a)||'').localeCompare(machineOf(b)||'','ja') || String(a.name||'').localeCompare(String(b.name||''),'ja'); }); var selRows=sel.map(function(id,i){ var d=byId[id]; return '
' +''+(i+1)+'' +''+esc(d.name)+''+(machineOf(d)?(' 🏭'+esc(machineOf(d))):'')+'' +'' +'' +'' +'
'; }).join(''); var restRows=rest.map(function(d){ return ''; }).join(''); host.innerHTML=(sel.length?('
✅ '+t('starter_selected')+'
'+selRows):'') +'
'+t('starter_addfrom')+'
' +'
'+(restRows||('
-
'))+'
' +''; } function starterToggle(id){ var i=STARTER_DRAFT.indexOf(id); if(i>=0)STARTER_DRAFT.splice(i,1); else STARTER_DRAFT.push(id); renderStarterEdit_(); } function starterMove(i,dir){ var j=i+dir; if(j<0||j>=STARTER_DRAFT.length)return; var t2=STARTER_DRAFT[i]; STARTER_DRAFT[i]=STARTER_DRAFT[j]; STARTER_DRAFT[j]=t2; renderStarterEdit_(); } async function starterSave(){ var st=document.getElementById('starter-stat'); if(st){ st.style.color='#0d5a8a'; st.textContent='💾 '+t('vm_saving'); } try{ var r=await docsGas('setStarter',{starter:JSON.stringify(STARTER_DRAFT)}); if(!r||!r.ok) throw new Error((r&&r.error)||'NG'); DOCS_SETTINGS.starter=JSON.stringify(STARTER_DRAFT); if(st){ st.textContent='✅ '+t('starter_saved'); } render(); // 一覧の🔰はじめにを更新 }catch(e){ if(st){ st.style.color='#a32820'; st.textContent='⚠ '+(e.message||e); } } } /* QRディープリンク(#doc=ID)で開いた時:その文書をハイライト&開く */ /* 📖 各アプリ「マニュアル棚」バナー(?app=xxx で来た時) */ const T_SHELF={ ja:{shelf_of:'のマニュアル・書類',shelf_all:'すべての文書',shelf_none:'このアプリ用のマニュアルはまだ登録されていません'}, en:{shelf_of:' — manuals & docs',shelf_all:'All documents',shelf_none:'No manuals registered for this app yet'}, vi:{shelf_of:' — tài liệu & hướng dẫn',shelf_all:'Tất cả tài liệu',shelf_none:'Chưa có tài liệu cho ứng dụng này'}, si:{shelf_of:' — අත්පොත් සහ ලේඛන',shelf_all:'සියලු ලේඛන',shelf_none:'මෙම යෙදුම සඳහා තවම අත්පොත් නැත'}, id:{shelf_of:' — manual & dokumen',shelf_all:'Semua dokumen',shelf_none:'Belum ada manual untuk aplikasi ini'}, fil:{shelf_of:' — mga manwal at dokumento',shelf_all:'Lahat ng dokumento',shelf_none:'Wala pang manwal para sa app na ito'}, zh:{shelf_of:'的手册与文件',shelf_all:'全部文件',shelf_none:'尚未为此应用登记手册'} }; Object.keys(T_SHELF).forEach(function(l){ if(TR[l]) Object.assign(TR[l],T_SHELF[l]); }); function renderAppBanner(){ var b=document.getElementById('app-banner'); if(!b) return; if(!APP_FILTER){ b.style.display='none'; b.innerHTML=''; return; } b.style.display='block'; b.innerHTML='
' +''+esc(appLabel(APP_FILTER))+t('shelf_of')+'' +'' +'
'; } function clearAppFilter(){ APP_FILTER=''; try{ history.replaceState(null,'',location.pathname); }catch(e){} renderAppBanner(); render(); } function applyDocDeepLink(){ try{ var h=(location.hash||'').match(/[#&]doc=([^&]+)/); if(!h) return; var id=decodeURIComponent(h[1]); var d=DOCS.find(function(x){return x.id===id;}); if(!d){ return; } // 権限外で一覧に無い等 showTab('list'); openDoc(id); }catch(e){} } /* ===================== 🎥 動画マニュアル(v2.5・B案) ===================== 本流=撮る→Gemini(動画を直接解析)が手順書(ステップ+時間+注意+7言語)→確認→Drive保存+文書登録。 閲覧=動画+手順リスト(言語ピッカー・時間ジャンプ)+この動画へのAI質問(docai)。再生はlogplayで記録。 */ const VM_LANGS=['ja','en','vi','si','id','fil','zh']; const VM_LANG_LABEL={ja:'日本語',en:'English',vi:'Tiếng Việt',si:'සිංහල',id:'Indonesia',fil:'Filipino',zh:'中文'}; function geminiKey(){ return (localStorage.getItem('docs_gemini_key')||'').trim(); } function saveGeminiKey(){ var v=(document.getElementById('set-gemini-key').value||'').trim(); localStorage.setItem('docs_gemini_key',v); var s=document.getElementById('set-gemini-stat'); if(s) s.textContent=v?('✅ '+t('vm_key_saved')):t('vm_key_cleared'); } /* 🎬 複数クリップ対応(社長指示2026-07-05「5〜6本をまとめて編集してつなげる」) VM_FILES=[{file,name,size,dur}](この順で1本の手順書につながる) VM_STEPS={title,machine,steps:[{clip,ct,warn,tr:{7言語}}],tips:[{tr}],transcript,confidential,confidentialReason} ※ステップは「クリップ番号 clip+そのクリップ内の秒数 ct」で持ち、保存時に前のクリップ長を足して通し秒数 t にする(再エンコード無し=つないで見せるだけ) */ let VM_FILES=[], VM_STEPS=null; const VM_MAX_CLIPS=12; var VM_AI_MAX=2*1024*1024*1024; // Gemini Files APIの実質上限(2GB・約1時間) function vmCircle_(n){ var c='①②③④⑤⑥⑦⑧⑨⑩⑪⑫'; return (n>=1&&n<=12)?c.charAt(n-1):('('+n+')'); } function vmSize_(n){ return n>=1024*1024*1024 ? (n/1024/1024/1024).toFixed(2)+'GB' : (n/1024/1024).toFixed(1)+'MB'; } function vmPick(ev){ var fl=(ev&&ev.target&&ev.target.files)?ev.target.files:null; if(fl&&fl.length) vmAddFiles_(fl); if(ev&&ev.target) ev.target.value=''; } function vmDrop(ev){ /* 🖱 D&D標準:PCから動画ファイルを直接ドロップ(複数OK) */ ev.preventDefault(); var dz=document.getElementById('vm-drop'); if(dz)dz.style.background=''; var fl=(ev.dataTransfer&&ev.dataTransfer.files)?ev.dataTransfer.files:null; var vids=[]; if(fl) for(var i=0;i=VM_MAX_CLIPS){ over=true; break; } var f=arr[i]; var dur=await vmDur_(f); VM_FILES.push({file:f,name:f.name,size:f.size,dur:dur}); } if(over) alert(t('vm_max_clips').replace('{n}',VM_MAX_CLIPS)); VM_STEPS=null; document.getElementById('vm-result').innerHTML=''; vmClipsRender_(); vmClipsToggleBtns_(); } function vmClipsToggleBtns_(){ var on=VM_FILES.length?'block':'none'; var r=document.getElementById('vm-run'); if(r) r.style.display=on; var s=document.getElementById('vm-skip'); if(s) s.style.display=on; } function vmClipMove_(i,d){ var j=i+d; if(j<0||j>=VM_FILES.length) return; var x=VM_FILES[i]; VM_FILES[i]=VM_FILES[j]; VM_FILES[j]=x; VM_STEPS=null; document.getElementById('vm-result').innerHTML=''; vmClipsRender_(); } function vmClipDel_(i){ VM_FILES.splice(i,1); VM_STEPS=null; document.getElementById('vm-result').innerHTML=''; vmClipsRender_(); vmClipsToggleBtns_(); } function vmClipsRender_(){ var box=document.getElementById('vm-clips'); if(!box) return; if(!VM_FILES.length){ box.innerHTML=''; var pv=document.getElementById('vm-preview'); if(pv){pv.style.display='none';pv.src='';} var fi=document.getElementById('vm-fileinfo'); if(fi) fi.textContent=''; return; } var total=VM_FILES.reduce(function(s,c){return s+(c.dur||0);},0); box.innerHTML='
🎬 '+t('vm_cliplist').replace('{n}',VM_FILES.length)+' '+t('vm_total_len')+' '+vmSec_(total)+'
' +VM_FILES.map(function(c,i){ return '
' +''+vmCircle_(i+1)+'' +''+esc(c.name)+' '+vmSize_(c.size)+(c.dur?('・'+vmSec_(c.dur)):'')+'' +'' +'' +'' +'
'; }).join('') +(VM_FILES.length>1?'
'+t('vm_multi_hint')+'
':'') +((total>3660||VM_FILES.some(function(c){return c.file.size>VM_AI_MAX;}))?('
⚠ '+t('vm_ai_limit_note')+'
'):''); } function vmSkipAI(){ /* 📼 AIなしで登録:撮るだけ先に済ませ、手順やナレーションは後からゆっくり */ if(!VM_FILES.length){ alert(t('vm_no_file')); return; } VM_STEPS={title:(VM_FILES[0].name||'').replace(/\.[^.]+$/,''),machine:'',steps:[],tips:[],transcript:''}; vmRenderResult_(); } function vmB64_(file){ return new Promise((res,rej)=>{ const r=new FileReader(); r.onload=()=>res(String(r.result).split(',')[1]); r.onerror=rej; r.readAsDataURL(file); }); } /* Gemini Files API(14MB超はinline不可→resumableアップロード→ACTIVE待ち→file_uri) */ async function vmUploadToGemini_(file,key){ var st=await fetch('https://generativelanguage.googleapis.com/upload/v1beta/files?key='+encodeURIComponent(key),{ method:'POST', headers:{'X-Goog-Upload-Protocol':'resumable','X-Goog-Upload-Command':'start', 'X-Goog-Upload-Header-Content-Length':String(file.size),'X-Goog-Upload-Header-Content-Type':file.type||'video/mp4', 'Content-Type':'application/json'}, body:JSON.stringify({file:{display_name:file.name}})}); var upUrl=st.headers.get('X-Goog-Upload-URL')||st.headers.get('x-goog-upload-url'); if(!upUrl) throw new Error('upload url not returned'); var up=await fetch(upUrl,{method:'POST',headers:{'X-Goog-Upload-Command':'upload, finalize','X-Goog-Upload-Offset':'0'},body:file}); var uj=await up.json(); var fmeta=uj.file||{}; var name=fmeta.name, uri=fmeta.uri, state=fmeta.state; var tries=0; while(state && state!=='ACTIVE' && tries<30){ await new Promise(r=>setTimeout(r,2000)); tries++; var g=await fetch('https://generativelanguage.googleapis.com/v1beta/'+name+'?key='+encodeURIComponent(key)); var gj=await g.json(); state=gj.state; uri=gj.uri||uri; } if(state!=='ACTIVE') throw new Error('video not ready (state='+state+')'); return {uri:uri, mime:file.type||'video/mp4'}; } /* v2.7: 大容量ファイルのDrive直接アップロード(GASの30MB制限回避) GASが発行した「再開可能アップロードURL」へ、ブラウザから32MBずつ直接PUT。 認可はURLに埋め込み=トークンは端末に出ない。失敗チャンクは3回まで自動リトライ。 */ async function vmDriveUpload_(blob,name,mime,scope,onp){ var init=await docsGas('initUpload',{fileName:name,mime:mime,size:blob.size}); if(!init||!init.ok) throw new Error((init&&init.error)||'アップロード開始NG'); var url=init.uploadUrl, CH=32*1024*1024, off=0, fid=null; while(off0&&ct>dur) ct=dur; return Math.max(0,Math.floor(ct)); } /* 1クリップをGeminiに読ませて手順書JSONを得る(inline≤14MB/それ超はFiles API) */ async function vmAnalyzeOne_(file,key,setstat){ var part; if(file.size<=14*1024*1024){ var b64=await vmB64_(file); part={inline_data:{mime_type:file.type||'video/mp4',data:b64}}; } else { setstat('📤 '+t('vm_uploading')); var up=await vmUploadToGemini_(file,key); part={file_data:{mime_type:up.mime,file_uri:up.uri}}; setstat('🤖 '+t('vm_analyzing')); } var res=await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='+encodeURIComponent(key),{ method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({contents:[{parts:[part,{text:vmPrompt_()}]}],generationConfig:{temperature:0.2,maxOutputTokens:8000}})}); var j=await res.json(); if(j.error) throw new Error(j.error.message||'Gemini error'); var txt=((((j.candidates||[])[0]||{}).content||{}).parts||[]).map(p=>p.text||'').join(''); txt=txt.replace(/```json|```/g,'').trim(); var m=txt.match(/\{[\s\S]*\}/); if(!m) throw new Error(t('vm_parse_err')); var o=JSON.parse(m[0]); if(!o||typeof o!=='object') throw new Error(t('vm_parse_err')); if(!Array.isArray(o.steps)) o.steps=[]; return o; } async function vmAnalyze(){ if(!VM_FILES.length){ alert(t('vm_no_file')); return; } if(VM_FILES.some(function(c){return c.file.size>VM_AI_MAX;})){ alert(t('vm_ai_too_big')); return; } // 保存はできる・AI解析だけ上限(正直に) var key=geminiKey(); if(!key){ alert(t('vm_no_key')); showTab('set'); return; } var st=document.getElementById('vm-stat'); if(window.showBusy) window.showBusy('🤖 '+t('vm_analyzing')); try{ var merged={title:'',machine:'',steps:[],tips:[],confidential:false,confidentialReason:'',transcript:'',_errs:[]}; for(var ci=0;ci1?('('+(ci+1)+'/'+VM_FILES.length+') '):''); var setstat=(function(p){ return function(msg){ st.textContent=p+msg; var bm=document.getElementById('busy-msg'); if(bm) bm.textContent='🤖 '+p+t('vm_analyzing'); }; })(pfx); setstat('🤖 '+t('vm_analyzing')); var o; try{ o=await vmAnalyzeOne_(VM_FILES[ci].file,key,setstat); } catch(e){ merged._errs.push((ci+1)+': '+(e.message||e)); continue; } if(!merged.machine && o.machine) merged.machine=o.machine; if(!merged.title && o.title) merged.title=o.title; if(o.confidential){ merged.confidential=true; if(!merged.confidentialReason && o.confidentialReason) merged.confidentialReason=o.confidentialReason; } if(o.transcript) merged.transcript+=(merged.transcript?'\n':'')+String(o.transcript); (function(idx){ (o.steps||[]).forEach(function(s){ merged.steps.push({clip:idx,ct:vmClampCt_(s.t,VM_FILES[idx].dur),warn:!!s.warn,tr:s.tr||{ja:''}}); }); (o.tips||[]).forEach(function(p){ merged.tips.push({tr:p.tr||{ja:''}}); }); })(ci); } if(!merged.steps.length && merged._errs.length) throw new Error(t('vm_parse_err')); if(!merged.title) merged.title=(VM_FILES[0].name||'').replace(/\.[^.]+$/,''); VM_STEPS=merged; if(merged.machine && !document.getElementById('vm-machine').value) document.getElementById('vm-machine').value=merged.machine; st.textContent=merged._errs.length?('⚠ '+t('vm_some_failed').replace('{list}',merged._errs.join(' / '))):''; vmRenderResult_(); }catch(e){ st.textContent='⚠ '+(e.message||e); } finally{ if(window.hideBusy) window.hideBusy(); } } function vmSec_(s){ s=Math.max(0,Math.floor(Number(s)||0)); return Math.floor(s/60)+':'+('0'+(s%60)).slice(-2); } function vmStepRow_(s,i){ return '
' +'
' +'' +'' +'' +'
' +'
🔊' +'
' +'
'; } function vmEditSc(i,v){ v=String(v||'').trim(); if(!VM_STEPS.steps[i])return; if(v) VM_STEPS.steps[i].sc={ja:v}; else delete VM_STEPS.steps[i].sc; } // v3.1: 🔊読み上げ文 function vmRenderResult_(){ var o=VM_STEPS; if(!o) return; var multi=VM_FILES.length>1; // クリップごとにグループ表示(複数のとき)。ステップは平坦配列で clip を持つ=index はそのまま使える var rows=''; if(multi){ for(var c=0;c '+(VM_FILES[c].dur?vmSec_(VM_FILES[c].dur):'')+''; o.steps.forEach(function(s,i){ if((s.clip||0)===c) rows+=vmStepRow_(s,i); }); rows+=''; } } else { rows=o.steps.map(function(s,i){ return vmStepRow_(s,i); }).join('') +''; } var tips=(o.tips||[]).map(function(p,i){ return '
' +'
'; }).join(''); // 🔐 AIの機密判定(confidential=true なら赤警告+🔒社外秘を自動提案。最終判断は人=変更できる) var confWarn = o.confidential ? '
' +'
🔐 '+t('vm_conf_warn')+'
' +(o.confidentialReason?'
🤖 '+esc(o.confidentialReason)+'
':'') +'
'+t('vm_conf_hint')+'
' : ''; document.getElementById('vm-result').innerHTML='
' +'

🤖 '+t('vm_result_title')+' 🤖 '+t('vm_check_badge')+'

' +confWarn +'' +''+rows +''+(tips||'
-
') +'' +'' +'' +'
'+t('vm_secrecy_rule')+'
' +'' +'
' +'
' +'
'; vmScopeHint_(); } /* 秘匿度の説明を選択に応じて出す+社外秘はAI検索を自動オフ */ function vmScopeHint_(){ var s=document.getElementById('vm-scope'); var h=document.getElementById('vm-scope-hint'); if(!s||!h) return; if(s.value==='secret'){ h.innerHTML='🔒 '+t('vm_secret_note'); h.style.color='#a32820'; h.style.background='#fdf3f2'; var a=document.getElementById('vm-aiok'); if(a) a.checked=false; } else { h.innerHTML=t('vm_secrecy_rule'); h.style.color='#8a5a00'; h.style.background='#fdf6e8'; } } function vmEditT(i,v){ if(!VM_STEPS.steps[i]) return; var m=String(v).match(/(\d+):(\d{1,2})/); var val=m ? (Number(m[1])*60+Number(m[2])) : (Number(v)||0); var c=VM_STEPS.steps[i].clip||0; VM_STEPS.steps[i].ct=vmClampCt_(val,(VM_FILES[c]&&VM_FILES[c].dur)||0); } function vmEditStep(i,v){ if(VM_STEPS.steps[i]){ var c=VM_STEPS.steps[i].clip||0; var sc=VM_STEPS.steps[i].sc; VM_STEPS.steps[i].tr={ja:v}; VM_STEPS.steps[i].clip=c; if(sc)VM_STEPS.steps[i].sc=sc; } } // 日本語を直したら他言語破棄→保存時にAIが再翻訳(読み上げ文scは保持) function vmEditTip(i,v){ if(VM_STEPS.tips&&VM_STEPS.tips[i]){ VM_STEPS.tips[i].tr={ja:v}; } } function vmDelStep(i){ VM_STEPS.steps.splice(i,1); vmRenderResult_(); } function vmDelTip(i){ (VM_STEPS.tips||[]).splice(i,1); vmRenderResult_(); } function vmAddStep(clip){ clip=Number(clip)||0; var lastCt=0; VM_STEPS.steps.forEach(function(s){ if((s.clip||0)===clip) lastCt=Math.max(lastCt,s.ct||0); }); VM_STEPS.steps.push({clip:clip,ct:lastCt+5,warn:false,tr:{ja:''}}); vmRenderResult_(); } function vmAddTip(){ VM_STEPS.tips=VM_STEPS.tips||[]; VM_STEPS.tips.push({tr:{ja:''}}); vmRenderResult_(); } function vmReset(){ VM_FILES=[]; VM_STEPS=null; var r=document.getElementById('vm-result'); if(r) r.innerHTML=''; var pv=document.getElementById('vm-preview'); if(pv){pv.style.display='none';pv.src='';} var run=document.getElementById('vm-run'); if(run) run.style.display='none'; var sk=document.getElementById('vm-skip'); if(sk) sk.style.display='none'; var cl=document.getElementById('vm-clips'); if(cl) cl.innerHTML=''; var fi=document.getElementById('vm-fileinfo'); if(fi) fi.textContent=''; var f1=document.getElementById('vm-file'); if(f1) f1.value=''; var f2=document.getElementById('vm-file2'); if(f2) f2.value=''; } function vmOcrText_(o,machine){ var L=['【動画マニュアル】'+(o.title||''),'機械: '+(machine||o.machine||''),'','■手順']; o.steps.forEach(function(s,i){ L.push((i+1)+'. ['+vmSec_(s.t)+'] '+((s.tr&&s.tr.ja)||'')+(s.warn?'(⚠注意)':'')); }); if(o.tips&&o.tips.length){ L.push('','■コツ・注意'); o.tips.forEach(function(p){ L.push('・'+((p.tr&&p.tr.ja)||'')); }); } if(o.transcript){ L.push('','■説明の書き起こし',String(o.transcript).slice(0,8000)); } return L.join('\n').slice(0,40000); } /* 1クリップをアップロード(≤25MB=GAS経由/超=Driveへ分割)→ {fileId,fileUrl,...} */ async function vmUploadClip_(file,scope,st,lbl){ var u; if(file.size<=25*1024*1024){ if(st) st.textContent='📤 '+t('vm_uploading_drive')+lbl; var b64=await vmB64_(file); u=await docsGas('upload',{dataB64:b64,mime:file.type||'video/mp4',fileName:file.name,scope:scope}); } else { if(window.showBusy) window.showBusy('📤 '+t('vm_uploading_drive')+lbl); try{ u=await vmDriveUpload_(file,file.name,file.type||'video/mp4',scope,function(p){ if(st) st.textContent='📤 '+t('vm_uploading_drive')+lbl+' '+p+'%'; var bm=document.getElementById('busy-msg'); if(bm) bm.textContent='📤 '+t('vm_uploading_drive')+lbl+' '+p+'%'; }); } finally{ if(window.hideBusy) window.hideBusy(); } } return u; } async function vmSave(){ if(!VM_FILES.length||!VM_STEPS){ return; } var st=document.getElementById('vm-save-stat'); st.textContent='💾 '+t('vm_saving'); try{ var multi=VM_FILES.length>1; // 手で足した/直した行の7言語をAIで埋めてから保存(空行は除外) VM_STEPS.steps=VM_STEPS.steps.filter(s=>((s.tr&&s.tr.ja)||'').trim()); VM_STEPS.tips=(VM_STEPS.tips||[]).filter(p=>((p.tr&&p.tr.ja)||'').trim()); st.textContent='🌐 '+t('vm_translating'); await vmFill7_(VM_STEPS); st.textContent='💾 '+t('vm_saving'); var scope=document.getElementById('vm-scope').value; // 🔒 社外秘は安全経路(GAS)がdoc1本=1ファイル前提。複数クリップの社外秘は未対応(正直に) if(scope==='secret' && multi){ st.textContent='⚠ '+t('vm_secret_single'); return; } if(scope==='secret' && VM_FILES[0].file.size>30*1024*1024){ st.textContent='⚠ '+t('vm_secret_size'); return; } if(scope==='secret' && !confirm(t('vm_secret_confirm'))) { st.textContent=''; return; } // クリップごとにアップロード(この順=再生順) var offs=[], acc=0, vids=[]; for(var i=0;i=1073741824) return (n/1073741824).toFixed(2)+'GB'; if(n>=1048576) return (n/1048576).toFixed(1)+'MB'; return Math.max(1,Math.round(n/1024))+'KB'; } async function loadOfflineIds_(){ try{ var l=await idbAll_(); OFFLINE_IDS=new Set(l.map(function(x){return x.key;})); render(); }catch(e){} } /* GAS getchunk を繰り返してファイル1本を Blob に組み立てる(進捗% コールバック・キャンセル対応) */ async function dlBlob_(id,narr,onp){ var CH=6*1024*1024, parts=[], start=0, total=0, mime=''; for(;;){ if(_dlAbort) throw new Error(t('dl_canceled')); var r=await docsGas('getchunk',{id:id,narr:narr?1:0,start:start,len:CH}); if(!r||!r.ok) throw new Error((r&&r.error)||'NG'); if(r.total) total=r.total; if(r.mime) mime=r.mime; var u8=b64ToU8_(r.b64||''); parts.push(u8); start+=u8.length; if(onp&&total) onp(Math.min(99,Math.round(start/total*100))); if(u8.length===0) break; if(total && start>=total) break; if(!total && u8.length📥 '+t('dl_saving')+' '+p+'% '); } async function vvSaveOffline(){ if(!VV_DOC||_dlBusy) return; var d=VV_DOC; _dlBusy=true; _dlAbort=false; _dlProg(0); try{ if(navigator.storage&&navigator.storage.persist){ try{ await navigator.storage.persist(); }catch(e){} } var vid=await dlBlob_(d.id,false,_dlProg); var narrBlob=null,narrMime=''; if(d.narrId){ _dlStat('🎙 '+t('dl_saving')+'…'); try{ var nb=await dlBlob_(d.id,true,null); narrBlob=nb.blob; narrMime=nb.mime; }catch(e){} } await idbPut_(d.id,{blob:vid.blob,narrBlob:narrBlob,name:d.name,mime:vid.mime,narrMime:narrMime,size:vid.blob.size+(narrBlob?narrBlob.size:0),savedAt:Date.now()}); OFFLINE_IDS.add(d.id); try{ render(); }catch(e){} _dlStat('✅ '+t('dl_saved')+''); vvInitPlayback_(d); // その場でオフライン再生へ切替 }catch(e){ var q=(e&&(e.name==='QuotaExceededError'||/quota|exceed/i.test((e.message||'')+(e.name||'')))); _dlStat('⚠ '+esc(q?t('dl_full'):String(e.message||e))+''); }finally{ _dlBusy=false; } } async function vvRemoveOffline(){ if(!VV_DOC) return; if(!confirm('🗑 '+t('dl_remove_q'))) return; var id=VV_DOC.id; try{ await idbDel_(id); OFFLINE_IDS.delete(id); }catch(e){} try{ render(); }catch(e){} vvClose(); openVideoViewer(id); // 通常(ストリーミング)再生で開き直す } /* 再生ソースを決める:オフライン保存があればBlob→無ければ社外秘=安全経路/通常=Driveストリーミング */ async function vvInitPlayback_(d){ var v=document.getElementById('vv-video'); var dlBtn=document.getElementById('vv-dl-btn'); var badge=document.getElementById('vv-offline-badge'); var rec=null; try{ rec=await idbGet_(d.id); }catch(e){} if(rec&&rec.blob&&v){ try{ if(window._vvVidBlobUrl) URL.revokeObjectURL(window._vvVidBlobUrl); }catch(e){} window._vvVidBlobUrl=URL.createObjectURL(rec.blob); v.style.display='block'; v.src=window._vvVidBlobUrl; if(rec.narrBlob){ try{ if(window._vvNarrBlobUrl) URL.revokeObjectURL(window._vvNarrBlobUrl); }catch(e){} window._vvNarrBlobUrl=URL.createObjectURL(rec.narrBlob); } vvNarrWire_(); if(badge){ badge.style.display=''; badge.textContent='📥 '+t('dl_offline_playing'); } if(dlBtn){ dlBtn.textContent='🗑 '+t('dl_remove'); dlBtn.onclick=vvRemoveOffline; } return; } if(badge) badge.style.display='none'; if(dlBtn){ dlBtn.textContent='📥 '+t('dl_save'); dlBtn.onclick=vvSaveOffline; } if(d.scope==='secret'){ vvLoadSecret_(d); } else { vvNarrWire_(); } } /* 設定:この端末の保存済み動画の管理 */ async function renderOfflineList_(){ var host=document.getElementById('offline-list'); if(!host) return; var list=[]; try{ list=await idbAll_(); }catch(e){} if(!list.length){ host.innerHTML='
'+t('dl_none')+'
'; return; } var total=list.reduce(function(s,x){ return s+((x.val&&x.val.size)||0); },0); var est=''; try{ if(navigator.storage&&navigator.storage.estimate){ var e=await navigator.storage.estimate(); est=' '+t('dl_device')+' '+fmtBytes_(e.usage||0)+' / '+fmtBytes_(e.quota||0); } }catch(e){} host.innerHTML='
📥 '+t('dl_total')+':'+fmtBytes_(total)+est+'
' + list.map(function(x){ return '
🎥 '+esc((x.val&&x.val.name)||x.key)+' '+fmtBytes_(x.val&&x.val.size)+'
'; }).join('') + ''; } async function offlineDel_(id){ if(!confirm('🗑 '+t('dl_remove_q'))) return; try{ await idbDel_(id); OFFLINE_IDS.delete(id); }catch(e){} renderOfflineList_(); try{render();}catch(e){} } async function offlineDelAll_(){ if(!confirm('🗑 '+t('dl_delall_q'))) return; try{ var l=await idbAll_(); for(var i=0;ix.id===id); if(!d)return; VV_DOC=d; docsGas('logplay',{id:d.id}).catch(function(){}); // 視聴記録(教育進捗の素・失敗しても再生は続行) if(!MY_PLAYS.has(d.id)){ MY_PLAYS.add(d.id); try{ render(); }catch(e){} } // v2.9: 開いた瞬間に「✅見た」へ(本人の未読が即消える) var m=document.getElementById('editModal'); var lang=''; m.innerHTML='
' +'

🎥 '+esc(d.name)+'

' +'
' +'' +'' +'
' +'
' +'
' +'
🌐'+lang +'' +'' +'' +'' +(canManage()?'':'') +(canManage()?'':'')+'
' +'' +'
' +'
📶 '+t('dl_hint')+'
' +'
🔊 '+t('autonarr_note')+'
' +'' +'
' +'
' +'' +'
' +'
' +'
' +(d.fileUrl?'':'')+'
' +'
'; m.style.display='flex'; vvRenderSteps_(); var _vv=vvSteps_(d); var _vids=(_vv&&_vv.videos)||[]; if(_vids.length>1){ vvInitMulti_(d,_vids); } // 🎬 複数クリップ=連結再生 else { vvInitPlayback_(d); } // v3.0: オフライン/社外秘/通常ストリーミング(単一クリップ) vvAutoInit_(); // v3.1: 🔊自動ナレーション(TTS)のボタン・配線(単一/複数どちらでも) } /* 🎬 複数クリップの連結再生(再エンコード無し=1本の