Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b9b7955
外部コンテンツ取得を検証済みの共通HTTPクライアントへ統一する
komagata Oct 6, 2026
2e6ce7e
コーディングテスト並び替え画面の「プラクティクス」を「プラクティス」に修正
Fuki3 Oct 11, 2026
7dd7e5e
日報テンプレートをユーザーごとに一意にした
samovar1214 Oct 12, 2026
78c3403
日報テンプレート関連の命名を整理する
samovar1214 Oct 12, 2026
dbe5227
日報テンプレートの登録後に再読み込みせず利用できるようにした
samovar1214 Oct 12, 2026
28c4d15
日報テンプレートの通信処理をrequest.jsに置き換えた
samovar1214 Oct 14, 2026
ea7d8dd
日報テンプレートの保存中に重複して送信されることを防いだ
samovar1214 Oct 14, 2026
de991bc
日報テンプレートの反映リンクをボタンに変更した
samovar1214 Oct 14, 2026
70f211b
承認済みPR通知のシェルへの直接展開を防ぐ
machida Oct 19, 2026
95a04c2
Bump shell-quote from 1.10.0 to 1.12.0
dependabot[bot] Oct 19, 2026
1972874
Merge pull request #10626 from fjordllc/dependabot/npm_and_yarn/shell…
komagata Oct 19, 2026
d09beb8
Merge pull request #10617 from fjordllc/bug/multiple-report-presets
komagata Oct 19, 2026
363f5aa
Merge pull request #10614 from fjordllc/bug/change_heading_in_sort_sc…
komagata Oct 19, 2026
6dc6865
外部コンテンツ取得とPjordツールを責務に沿って再配置する
komagata Oct 21, 2026
305a666
再利用するAIツールの名前空間をAiToolsに変更する
komagata Oct 21, 2026
15d3e64
大きな応答のリンク判定と日本語メタデータ取得を修正する
komagata Oct 21, 2026
115e2af
Merge pull request #10591 from fjordllc/security/shared-external-cont…
komagata Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions .github/workflows/update_only_when_reviewer_approved.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: post approved pr event
env:
FJORD_CHOICE_TOKEN: ${{ secrets.FJORD_CHOICE_TOKEN }}
PR_PAYLOAD: |
{"title": ${{ toJson(github.event.pull_request.title) }},
"number": ${{ github.event.pull_request.number }},
"state": ${{ toJson(github.event.pull_request.state) }},
"reviewer_uids": ${{ toJson(github.event.pull_request.requested_reviewers.*.id) }}
}
run: |
curl -f -X POST https://fjord-choice.herokuapp.com/api/pulls \
-H 'Content-Type: application/json' \
-H 'Authorization: Token ${{ secrets.FJORD_CHOICE_TOKEN }}' \
-d '{"title": ${{ toJson(github.event.pull_request.title) }},
"number": ${{ github.event.pull_request.number }},
"state": ${{ toJson(github.event.pull_request.state) }},
"reviewer_uids": ${{ toJson(github.event.pull_request.requested_reviewers.*.id) }}
}'
-H "Authorization: Token $FJORD_CHOICE_TOKEN" \
-d "$PR_PAYLOAD"
2 changes: 1 addition & 1 deletion app/agents/pjord/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class Pjord::Agent < RubyLLM::Agent
model ENV.fetch('PJORD_LLM_MODEL', 'claude-sonnet-5')
tools BootcampSearchTool, UserInfoTool, ExternalContentTool
tools AiTools::BootcampSearchTool, AiTools::UserInfoTool, AiTools::ExternalContentTool
schema PjordResponse
instructions { AiPrompt.body_for('pjord') }

Expand Down
193 changes: 109 additions & 84 deletions app/javascript/controllers/report_preset_controller.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
import { Controller } from '@hotwired/stimulus'
import { post, put } from '@rails/request.js'
import TextareaInitializer from 'textarea-initializer'
import CSRF from 'csrf'
import { toast } from 'vanillaToast'

export default class extends Controller {
static targets = [
'report',
'reportInput',
'modal',
'templateInput',
'presetInput',
'submitButton',
'templateTab',
'presetEditTab',
'previewTab',
'templateContent',
'previewContent'
'presetEditPanel',
'previewPanel',
'replaceReportAction',
'openModalButton'
]

static values = {
registeredTemplate: String,
registeredTemplateId: Number,
editing: Boolean,
isEditingTemplateValid: Boolean
presetId: Number,
savedPresetDescription: String
}

connect() {
if (this.reportTarget.value === '') {
this.reportTarget.value = this.registeredTemplateValue
if (this.reportInputTarget.value === '') {
this.reportInputTarget.value = this.savedPresetDescriptionValue
}
this.registeredTemplateValue = this.templateInputTarget.value
TextareaInitializer.initialize('.js-report-content')
this.updatePresetControls()
}

replaceReport(e) {
initialize() {
this.isSaving = false
}

replaceReportWithPreset(e) {
e.preventDefault()
if (
this.reportTarget.value === '' ||
this.reportInputTarget.value === '' ||
confirm('日報が上書きされますが、よろしいですか?')
) {
this.reportTarget.value = this.registeredTemplateValue
this.reportInputTarget.value = this.savedPresetDescriptionValue
TextareaInitializer.uninitialize('.js-report-content')
TextareaInitializer.initialize('.js-report-content')
}
Expand All @@ -46,123 +50,144 @@ export default class extends Controller {
if (e.target !== e.currentTarget) {
return
}
if (this.editingValue === this.registeredTemplateValue) {
if (
this.presetInputTarget.value === this.savedPresetDescriptionValue ||
confirm('テンプレートを登録せずに終了しますか?')
) {
this.closeModal()
} else {
confirm('テンプレートを登録せずに終了しますか?') && this.closeModal()
}
}

openModal(e) {
e.preventDefault()
this.modalTarget.classList.remove('is-hidden')
this.templateInputTarget.value = this.registeredTemplateValue
TextareaInitializer.initialize('#js-template-content')
this.presetInputTarget.value = this.savedPresetDescriptionValue
TextareaInitializer.initialize('#js-preset-content')
this.updateSubmitButtonState()
}

closeModal() {
this.modalTarget.classList.add('is-hidden')
}

registerTemplate(e) {
async savePreset(e) {
e.preventDefault()
const editingTemplate = this.templateInputTarget.value
if (this.isSaving) {
return
}

const editingDescription = this.presetInputTarget.value
if (
editingTemplate === '' ||
editingTemplate === this.registeredTemplateValue
editingDescription.trim() === '' ||
editingDescription.trim() === this.savedPresetDescriptionValue.trim()
) {
return null
return
}

this.isSaving = true
this.updateSubmitButtonState()

try {
if (this.presetIdValue > 0) {
await this.updatePreset(editingDescription)
} else {
await this.registerPreset(editingDescription)
}
} finally {
this.isSaving = false
this.updateSubmitButtonState()
}
}

registerPreset(editingDescription) {
const params = {
report_preset: { description: editingTemplate }
report_preset: { description: editingDescription }
}
fetch('/api/report_presets', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': CSRF.getToken()
},
credentials: 'same-origin',
redirect: 'manual',
body: JSON.stringify(params)

return post('/api/report_presets', {
body: params,
responseKind: 'json',
redirect: 'manual'
})
.then((response) => {
return response.json()
if (!response.ok) {
throw new Error('テンプレートの登録に失敗しました')
}
return response.json
})
.then((newTemplate) => {
this.registeredTemplateValue = editingTemplate
this.registeredTemplateIdValue = newTemplate.id
.then((createdPreset) => {
this.presetIdValue = createdPreset.id
this.savedPresetDescriptionValue = editingDescription
this.updatePresetControls()
toast('テンプレートを登録しました!')
this.closeModal()
})
.catch((error) => {
console.warn(error)
toast('テンプレートを登録できませんでした。', 'error')
})
}

updateTemplate(e) {
e.preventDefault()
const editingTemplate = this.templateInputTarget.value
if (
editingTemplate === '' ||
editingTemplate === this.registeredTemplateValue
) {
return null
}

updatePreset(editingDescription) {
const params = {
report_preset: { description: editingTemplate }
report_preset: { description: editingDescription }
}
fetch(`/api/report_presets/${this.registeredTemplateIdValue}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': CSRF.getToken()
},
credentials: 'same-origin',
redirect: 'manual',
body: JSON.stringify(params)

return put(`/api/report_presets/${this.presetIdValue}`, {
body: params,
responseKind: 'json',
redirect: 'manual'
})
.then(() => {
this.registeredTemplateValue = editingTemplate
.then((response) => {
if (!response.ok) {
throw new Error('テンプレートの変更に失敗しました')
}
this.savedPresetDescriptionValue = editingDescription
this.updatePresetControls()
toast('テンプレートを変更しました!')
this.closeModal()
})
.catch((error) => {
console.warn(error)
toast('テンプレートを変更できませんでした。', 'error')
})
}

clickTemplateTab(e) {
updatePresetControls() {
const isPresetRegistered = this.presetIdValue > 0
this.replaceReportActionTarget.classList.toggle(
'is-hidden',
!isPresetRegistered
)
this.openModalButtonTarget.textContent = isPresetRegistered
? 'テンプレート変更'
: 'テンプレート登録'
this.submitButtonTarget.textContent = isPresetRegistered ? '変更' : '登録'
this.updateSubmitButtonState()
}

updateSubmitButtonState() {
const editingDescription = this.presetInputTarget.value.trim()
const savedDescription = this.savedPresetDescriptionValue.trim()

const isValid =
editingDescription !== '' && editingDescription !== savedDescription
this.submitButtonTarget.disabled = this.isSaving || !isValid
}

openPresetEditTab(e) {
e.preventDefault()
this.editingValue = true
this.templateTabTarget.classList.add('is-active')
this.presetEditTabTarget.classList.add('is-active')
this.previewTabTarget.classList.remove('is-active')
this.templateContentTarget.classList.add('is-active')
this.previewContentTarget.classList.remove('is-active')
this.presetEditPanelTarget.classList.add('is-active')
this.previewPanelTarget.classList.remove('is-active')
}

clickPreviewTab(e) {
openPreviewTab(e) {
e.preventDefault()
this.editingValue = false
this.templateTabTarget.classList.remove('is-active')
this.presetEditTabTarget.classList.remove('is-active')
this.previewTabTarget.classList.add('is-active')
this.templateContentTarget.classList.remove('is-active')
this.previewContentTarget.classList.add('is-active')
}

updateSubmitButtonState() {
const editingTemplate = this.templateInputTarget.value.trim()
const registeredTemplate = this.registeredTemplateValue.trim()

const isValid =
editingTemplate !== '' && editingTemplate !== registeredTemplate
this.isEditingTemplateValidValue = isValid
this.submitButtonTarget.disabled = !isValid
this.presetEditPanelTarget.classList.remove('is-active')
this.previewPanelTarget.classList.add('is-active')
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class BootcampSearchTool < RubyLLM::Tool
class AiTools::BootcampSearchTool < RubyLLM::Tool
description 'bootcampのプラクティス(カリキュラム)、ドキュメント(Docs)、Q&A、お知らせを検索する。' \
'ユーザーの質問に答えるために必要な情報を探すときに使う。'

Expand All @@ -18,6 +18,10 @@ class BootcampSearchTool < RubyLLM::Tool
RESULT_LIMIT = 5
CONTENT_TRUNCATE_LENGTH = 500

def self.tool_name
'bootcamp_search'
end

def execute(query:, category: 'all')
targets = select_targets(category)
results = search(query, targets)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# frozen_string_literal: true

class ExternalContentTool < RubyLLM::Tool
class AiTools::ExternalContentTool < RubyLLM::Tool
description 'URLの先にある内容を確認する。GitHubのPR・コード・ディレクトリ、rawファイル、一般Webページを読み、回答やレビューに必要な文脈を取得するときに使う。'

parameter :url, description: '確認したいhttpまたはhttpsのURL'

def self.tool_name
'external_content'
end

def execute(url:)
ExternalContent.fetch(url)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# frozen_string_literal: true

class UserInfoTool < RubyLLM::Tool
class AiTools::UserInfoTool < RubyLLM::Tool
description 'メンションしてきたユーザーのプロフィールやカリキュラムの進捗状況を取得する。' \
'ユーザーに合わせた回答をするときに使う。'

parameter :login_name, description: 'ユーザーのログイン名'

def self.tool_name
'user_info'
end

def execute(login_name:)
user = User.find_by(login_name: login_name)
return "ユーザー「#{login_name}」が見つかりませんでした。" if user.nil?
Expand Down
File renamed without changes.
Loading
Loading